phplaravelexceptionlaravel-middleware

Handle exceptions and fatal errors into laravel middleware


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-&gt;handleError()
#1 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): App\Http\Controllers\API\v1\TestController-&gt;test()
#2 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(45): Illuminate\Routing\Controller-&gt;callAction()
#3 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Route.php(239): Illuminate\Routing\ControllerDispatcher-&gt;dispatch()
#4 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Route.php(196): Illuminate\Routing\Route-&gt;runController()
#5 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(685): Illuminate\Routing\Route-&gt;run()
#6 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Routing\Router-&gt;Illuminate\Routing\{closure}()
#7 /var/www/html/my_project/app/Http/Middleware/HandleErrors.php(16): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#8 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): App\Http\Middleware\HandleErrors-&gt;handle()
#9 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(41): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#10 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Routing\Middleware\SubstituteBindings-&gt;handle()
#11 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php(59): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#12 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Routing\Middleware\ThrottleRequests-&gt;handle()
#13 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#14 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(687): Illuminate\Pipeline\Pipeline-&gt;then()
#15 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(662): Illuminate\Routing\Router-&gt;runRouteWithinStack()
#16 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(628): Illuminate\Routing\Router-&gt;runRoute()
#17 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Routing/Router.php(617): Illuminate\Routing\Router-&gt;dispatchToRoute()
#18 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(165): Illuminate\Routing\Router-&gt;dispatch()
#19 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Foundation\Http\Kernel-&gt;Illuminate\Foundation\Http\{closure}()
#20 /var/www/html/my_project/app/Http/Middleware/corsMiddleware.php(16): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#21 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): App\Http\Middleware\corsMiddleware-&gt;handle()
#22 /var/www/html/my_project/vendor/pragmarx/tracker/src/Vendor/Laravel/Middlewares/Tracker.php(24): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#23 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): PragmaRX\Tracker\Vendor\Laravel\Middlewares\Tracker-&gt;handle()
#24 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#25 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\TransformsRequest-&gt;handle()
#26 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#27 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\TransformsRequest-&gt;handle()
#28 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php(27): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#29 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\ValidatePostSize-&gt;handle()
#30 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php(63): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#31 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode-&gt;handle()
#32 /var/www/html/my_project/vendor/fruitcake/laravel-cors/src/HandleCors.php(37): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#33 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Fruitcake\Cors\HandleCors-&gt;handle()
#34 /var/www/html/my_project/vendor/fideloper/proxy/src/TrustProxies.php(57): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#35 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Fideloper\Proxy\TrustProxies-&gt;handle()
#36 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}()
#37 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(140): Illuminate\Pipeline\Pipeline-&gt;then()
#38 /var/www/html/my_project/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(109): Illuminate\Foundation\Http\Kernel-&gt;sendRequestThroughRouter()
#39 /var/www/html/my_project/public/index.php(55): Illuminate\Foundation\Http\Kernel-&gt;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 :


Solution

  • 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.