0

I'm trying to modify my App\Exceptions\Handler to pass the request (and therefore current URL) through to all exceptions. For this reason I need the lowest-level exception class I can get hold of to type-hint to the ->renderable() method.

Laravel/Symfony's HttpException works but only for HTTP errors, leaving out all non-HTTP exceptions. PHP's Exception class works when using getCode() instead of getStatusCode(), but always returns a "0" for both HTTP errors and exceptions. Is there another low-level exception class that will work for my purposes, or otherwise any other way to accomplish what I'm trying to do here?

public function register()
{

$this->renderable(function (Exception $exception, $request) {

    $url = $request->fullUrl();
    $status = $exception->getCode();

    Log::warning("Error $status when trying to visit $url. Received the following message: " . $exception->getMessage()); 

    return response()->view("errors.$status", [
        "exception" => $exception
        ],
    $status
    );    
    });
   
    }

}

For what it's worth, I'm using the following web routes to trigger exceptions and HTTP errors for testing:

if (app()->environment('local')) {
    Route::get("/exception", function (){
        throw new JsonException; // chosen because it's one of the few Laravel exceptions 
                                 // that doesn't seem to automatically resolve to a HTTP error
    });
}

if (app()->environment('local')) {
    Route::get("/fail/{status}", function ($status){
        abort($status);
    });
}
Hashim Aziz
  • 4,074
  • 5
  • 38
  • 68
  • 2
    I don't think there is. I use `get_class($e)` in my Handler to match the type of exception, and tweak the response code as needed. – aynber Feb 14 '23 at 17:30
  • I don't know if it is helpful, but Symfony (and I assume thus Laravel) has the [`onKernelRequest`](https://symfony.com/doc/current/event_dispatcher.html#request-events-checking-types) event which has the original Request. We use this to create a UUID in an HTTP header for trace and exception logging, but you could also stash the requested URL somehow, and retrieve in an exception maybe? – Chris Haas Feb 14 '23 at 17:50
  • @aynber That's a shame, it seems crazy to me there's no simple way to do this for all exceptions. Would you mind posting the code of your Handler as an answer so I can see what that looks like in practice? – Hashim Aziz Feb 14 '23 at 21:15

3 Answers3

1

As requested, this is what I have in my Handler. I use some custom logging, and I want to make sure I grab the right code when it's an HTTP error.

public function report(Throwable $e)
{
    $code = match (get_class($e)) {
        'Symfony\Component\HttpKernel\Exception\NotFoundHttpException' => 404,
        \HttpException::class => $e->getStatusCode(),
        default => 'No Code',
    };
    // more stuff here 
 }

You can use $e->getCode() for your default as well

aynber
  • 22,380
  • 8
  • 50
  • 63
0

You can throw your JsonException and abort like so with a given code and the handler should grab it from getCode like so

// in your controller
throw new \JsonException('Something went wrong', 500);
// or
abort(500, 'Something went wrong')

// in your handler
$status = $e->getCode(); // 500
$message = $e->getMessage(); // "Something went wrong" 

That said it's better to keep them as semantically separate as possible in my opinion, and let the handler do the handling depending on what it receives.

Gavin
  • 2,214
  • 2
  • 18
  • 26
  • This doesn't seem to make any difference, `getCode` always returns a 0 in the exception handler, but more importantly, I'd like to be able to do this without manually passing error codes every time - most Laravel exceptions are automatically thrown and I think I'm correct in thinking this approach would return nothing for those. – Hashim Aziz Feb 14 '23 at 21:36
0

I finally managed to figure this out in the end. It's probably not the cleanest solution, but it works perfectly for my needs.

It works by inspecting each instance of the Exception class and using PHP's instanceof() to check whether it's a HTTP exception or not. If it is, it gets logged with the request URL and returns a view with a status code. If it's a generic non-HTTP exception, it gets logged with the request URL and returns another view with no status code (or you can keep the default exception behaviour by removing the return block, which renders a blank screen in production).

public function register()
{

$this->renderable(function (Exception $exception, $request) {

  $url = $request->fullUrl();

  if ($exception instanceof HttpException) {
  
  $status = $exception->getStatusCode();

  Log::warning("Error $status occurred when trying to visit $url. Received the following message: " . $exception->getMessage()); 

  return response()->view("errors.error", [
  "exception" => $exception,
  "status" => $status
  ],
  $status
  );

} else {

  $status = $exception->getCode();

  Log::warning("Exception $status occurred when trying to visit $url. Received the following message: " . $exception->getMessage()); 

  return response()->view("errors.exception", [
  "exception" => $exception,
  "status" => $status

  ]);
  }
});

// Optionally suppress all Laravel's default logging for exceptions, so only your own logs go to the logfile 
$this->reportable(function (Exception $e) {
})->stop();

}
Hashim Aziz
  • 4,074
  • 5
  • 38
  • 68