Handle exceptions and fatal errors into laravel middleware

361 Views Asked by At

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 :

  • Why the exception is handled correctly into the controller but not into the middleware ?
  • How can I handle syntax errors (and all possible exceptions and errors) and return a response with HTTP code 500 ?
1

There are 1 best solutions below

0
Hesam Rad On

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.