Arkaik
Arkaik

Reputation: 902

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 :

Upvotes: 0

Views: 990

Answers (1)

Hesam Rad
Hesam Rad

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

Related Questions