Reputation: 902
I'm currently developing an API using Laravel 7.30.6 and I'd like to be able to handle all errors and return a Internal server error with HTTP code 500 on all the API routes if anything goes wrong, but I want to keep the default error handler (app/Exceptions/Handler.php
) for non API related requests.
To do it I created a new Middleware HandleErrors
<?php
// app\Http\Middleware\HandleErrors.php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Response;
use Closure;
use Log;
class HandleErrors
{
public function handle(Request $request, Closure $next)
{
try
{
return $next($request);
}
catch (\Throwable $th)
{
Log::error($th);
return Response::json(array("message"=>"Internal server error"), 500);
}
}
}
I registered the middleware
<?php
// app\Http\Kernel.php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
[...]
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
[...]
'handle-errors' => \App\Http\Middleware\HandleErrors::class,
];
}
And I created a new route using this middleware calling a test controller
<?php
// routes/api.php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
// */
Route::middleware(["handle-errors"])->group(function () {
Route::get("/v1/test", "API\\v1\TestController@test");
});
Into the test controller, I'm voluntarily generating errors, such as Division by zero
<?php
// app/Http/Controllers/API/v1/TestController.php
namespace App\Http\Controllers\API\v1;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Response;
use Log;
class TestController extends Controller {
public function test(Request $request)
{
//$a = 10 / 0;
return Response::json(array("message"=>"SUCCESS"), 200);
}
}
When commenting the division by zero, I do receive the 'SUCCESS' response. However when de-commenting it, the Exception is not handled by the middleware and I receive the error stack as a response.
[...]
<!--
ErrorException: Division by zero in file /var/www/html/my_project/app/Http/Controllers/API/v1/TestController.php on line 15
#0 /var/www/html/my_project/app/Http/Controllers/API/v1/TestController.php(15): Illuminate\Foundation\Bootstrap\HandleExceptions->handleError()
#1 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): App\Http\Controllers\API\v1\TestController->test()
#2 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(45): Illuminate\Routing\Controller->callAction()
#3 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Route.php(239): Illuminate\Routing\ControllerDispatcher->dispatch()
#4 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Route.php(196): Illuminate\Routing\Route->runController()
#5 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(685): Illuminate\Routing\Route->run()
#6 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Routing\Router->Illuminate\Routing\{closure}()
#7 /var/www/html/my_project/app/Http/Middleware/HandleErrors.php(16): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#8 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): App\Http\Middleware\HandleErrors->handle()
#9 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(41): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#10 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Routing\Middleware\SubstituteBindings->handle()
#11 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php(59): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#12 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Routing\Middleware\ThrottleRequests->handle()
#13 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#14 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(687): Illuminate\Pipeline\Pipeline->then()
#15 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(662): Illuminate\Routing\Router->runRouteWithinStack()
#16 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(628): Illuminate\Routing\Router->runRoute()
#17 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(617): Illuminate\Routing\Router->dispatchToRoute()
#18 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(165): Illuminate\Routing\Router->dispatch()
#19 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Foundation\Http\Kernel->Illuminate\Foundation\Http\{closure}()
#20 /var/www/html/my_project/app/Http/Middleware/corsMiddleware.php(16): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#21 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): App\Http\Middleware\corsMiddleware->handle()
#22 /var/www/html/my_project/vendor/pragmarx/tracker/src/Vendor/Laravel/Middlewares/Tracker.php(24): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#23 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): PragmaRX\Tracker\Vendor\Laravel\Middlewares\Tracker->handle()
#24 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#25 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\TransformsRequest->handle()
#26 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#27 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\TransformsRequest->handle()
#28 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php(27): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#29 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\ValidatePostSize->handle()
#30 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php(63): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#31 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode->handle()
#32 /var/www/html/my_project/vendor/fruitcake/laravel-cors/src/HandleCors.php(37): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#33 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Fruitcake\Cors\HandleCors->handle()
#34 /var/www/html/my_project/vendor/fideloper/proxy/src/TrustProxies.php(57): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#35 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Fideloper\Proxy\TrustProxies->handle()
#36 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#37 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(140): Illuminate\Pipeline\Pipeline->then()
#38 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(109): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter()
#39 /var/www/html/my_project/public/index.php(55): Illuminate\Foundation\Http\Kernel->handle()
#40 {main}
-->
</body>
</html>
If I surround the test
function code with a try/catch block, I do receive the HTTP error 500 as expected.
public function test(Request $request)
{
try
{
$a = 10 / 0;
return Response::json(array("message"=>"SUCCESS"), 200);
}
catch (\Throwable $th)
{
Log::error($th);
return Response::json(array("message"=>"Internal server error"), 500);
}
}
I'd like to avoid surrounding all my API functions into a try/catch block and handle exception directly into the middleware.
I also realized that some errors were not correctly handled, for example if I create a syntax error, the error is not correctly handled within the catch block
So my questions are the following :
Upvotes: 0
Views: 990
Reputation: 47
Let me give you a better idea.
You could create a new Handler
class to pass to Laravel instead of its own Handler
class.
Let's call this new class, ApiHandler
and it lives right next to App/Exceptions/Handler.php
. (Note that ApiHandler
, extends Laravel's Handler
class.)
You could have this logic inside ApiHandler
class:
<?php
namespace App\Exceptions;
use \Throwable;
use Illuminate\Http\JsonResponse;
class ApiHandler extends Handler
{
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $e
* @return \Symfony\Component\HttpFoundation\Response
*/
public function render($request, Throwable $e)
{
return $this->shouldReturnJson($request, $e) ?
$this->prepareJsonResponse($request, $e) :
$this->prepareResponse($request, $e);
}
/**
* Prepare a JSON response for the given exception.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $e
* @return \Illuminate\Http\JsonResponse
*/
protected function prepareJsonResponse($request, Throwable $e)
{
$data = $this->convertExceptionToArray($e);
return new JsonResponse(
$this->convertExceptionToArray($e),
$data['status'] ?? 500,
$this->isHttpException($e) ? $e->getHeaders() : [],
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
);
}
/**
* Convert the given exception to an array.
*
* @param \Throwable $e
* @return array
*/
protected function convertExceptionToArray(Throwable $e)
{
$response = [
'code' => $this->isHttpException($e) ? $e->getCode() : 500,
'message' => $e->getMessage() ?? 'Internal Server Error',
];
if (env('APP_DEBUG')) {
$response ['file'] = $e->getFile();
$response ['line'] = $e->getLine();
$response ['trace'] = $e->getTrace();
}
return $response;
}
}
Now you have to tell Laravel to use this class, instead of its own.
Go to AppServiceProvider
and register ApiHandler
.
/**
* Register any application services.
*
* @return void
*/
public function register()
{
// Registering api handler instead of
// Laravel's built-in one. (Comment if want to revert)
$this->app->bind(
\Illuminate\Contracts\Debug\ExceptionHandler::class,
\App\Exceptions\ApiHandler::class
);
}
Now every time your application catches an exception, it will use this class and provide you the response structure you want. Note that I added a few details to create a better developer experience for you and your co-workers when debugging the system.
Cheers.
Upvotes: 1