Laravel api class exception handling - php

I have a Laravel 4.2 application with the following route:
Route::group(['prefix' => 'api/v1'], function()
{
Route::resource('service_logs', 'ServiceLogsController', [
'only' => ['index', 'store', 'show']
]);
});
The ServiceLogsController controller extends from ApiController, which looks something like this cut down version:
class ApiController extends \BaseController {
protected $statusCode = 200;
public function getStatusCode()
{
return $this->statusCode;
}
public function setStatusCode($statusCode)
{
$this->statusCode = $statusCode;
return $this;
}
public function respondInternalError($message = 'Internal Error!')
{
return $this->setStatusCode(500)->respondWithError($message);
}
public function respondWithError($message)
{
return Response::json([
'error' => [
'message' => $message,
'status_code' => $this->getStatusCode()
]
], $this->getStatusCode());
}
// ...
}
What I'd like to do is, when ever an un-caught exception occurs, I want to call the respondInternalError() method on my ApiController, so that the API consumer has a consistent response rather than nothing or html whoops error.
To achieve this, I tried adding the following code in my app/start/global.php
App::error(function(Exception $exception, $code)
{
Log::error($exception);
if (Request::is('api/*'))
{
App::make('ApiController')->respondInternalError('Uncaught api exception error occurred - ' . $exception->getMessage());
}
});
and to test it, I tried to make a POST request to the following url: /api/v1/service_logs/123.
this will not work, because that URL is a GET url, so Laravel throws the correct method not allowed exception. However, it's not getting caught.
Any idea how to implement a catch all exception handler per controller class basis?
Update Slightly improved working "global" api exception handler
App::error(function(Exception $exception, $code)
{
Log::error($exception);
if (Request::is('api/*'))
{
$errorName = Symfony\Component\HttpFoundation\Response::$statusTexts[$code];
return App::make('ApiController')
->setStatusCode($code)
->respondWithError($errorName . ' error has occurred.');
}
});
When an error occurs, you get this now (testing done in Chrome + Postman):

The solution is actually very very simple. You only need to return the return value of your controller function
if (Request::is('api/*'))
{
return App::make('ApiController')->respondInternalError('Uncaught api exception error occurred - ' . $exception->getMessage());
}

Related

render function in Handler.php not working Laravel 8

I want to return a JSON response instead of the default 404 error page when ModelNotFoundException occurs. To do this, I wrote the following code into app\Exceptions\Handler.php :
public function render($request, Exception $exception)
{
if ($exception instanceof ModelNotFoundException) {
return response()->json([
'error' => 'Resource not found'
], 404);
}
return parent::render($request, $exception);
}
However it doesn't work. When the ModelNotFoundException occurs, Laravel just shows a blank page. I find out that even declaring an empty render function in Handler.php makes Laravel display a blank page on ModelNotFoundException.
How can I fix this so it can return JSON/execute the logic inside the overriden render function?
In Laravel 8x, You need to Rendering Exceptions in register() method
use App\Exceptions\CustomException;
/**
* Register the exception handling callbacks for the application.
*
* #return void
*/
public function register()
{
$this->renderable(function (CustomException $e, $request) {
return response()->view('errors.custom', [], 500);
});
}
For ModelNotFoundException you can do it as below.
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
public function register()
{
$this->renderable(function (NotFoundHttpException $e, $request) {
return response()->json(...);
});
}
By default, the Laravel exception handler will convert exceptions into an HTTP response for you. However, you are free to register a custom rendering Closure for exceptions of a given type. You may accomplish this via the renderable method of your exception handler. Laravel will deduce what type of exception the Closure renders by examining the type-hint of the Closure:
More info about the error exception
This code doesn't work for me (in Laravel 8.74.0):
$this->renderable(function (ModelNotFoundException$e, $request) {
return response()->json(...);
});
Don't know why, but ModelNotFoundException is directly forwarded to NotFoundHttpException (which is a part of Symfony Component) that used by Laravel and will ultimately triggers a 404 HTTP response. My workaround is checking the getPrevious() method of the exception:
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*')) {
if ($e->getPrevious() instanceof ModelNotFoundException) {
return response()->json([
'status' => 204,
'message' => 'Data not found'
], 200);
}
return response()->json([
'status' => 404,
'message' => 'Target not found'
], 404);
}
});
And then we will know that this exception come from ModelNotFoundException and return a different response with NotFoundHttpException.
Edit
This is why ModelNotFoundException thrown as NotFoundHttpException
This one is my Handler file:
use Throwable;
public function render($request, Throwable $exception)
{
if( $request->is('api/*')){
if ($exception instanceof ModelNotFoundException) {
$model = strtolower(class_basename($exception->getModel()));
return response()->json([
'error' => 'Model not found'
], 404);
}
if ($exception instanceof NotFoundHttpException) {
return response()->json([
'error' => 'Resource not found'
], 404);
}
}
}
This one is only for all request in API route. If you want to catch all request, so remove the first if.
Please note that by default Laravel emits a JSON representation of an exception ONLY when you send a request with the header parameter Accept: application/json! For all other requests, Laravel sends normal HTML rendered output.

Change message of invalid scope

How can I change invalid scope(s) provided message in Laravel Passport
try to update this render function in app/Exceptions/Handler.php file
public function render($request, Exception $exception)
{
if ($exception instanceof \Laravel\Passport\Exceptions\MissingScopeException)
{
return response()->json(['message' => 'your message here']);
//abort(401);
}
return parent::render($request, $e);
}
You can catch that as an Exception like so in your controller:
try {
// Whatever you are doing which leads to such error.
} catch (MissingScopeException $e) {
return response()->json(['message' => 'YOUR DESIRED MESSAGE.']);
}
By the way, this is where leads to such exception.
I'm using laravel/framework v8.10.0 and laravel/passport v10.0.1.
If you are using Passport only for API you could change directly the throw exception in the Middleware.
Steps:
Go to app/Http/Kernel.php.
In the $routeMiddleware array you need to have the line:
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class.
Go to the CheckForAnyScope middleware with Ctrl + click (Windows) or Cmd + click (MacOS).
In the CheckForAnyScope middleware go to handle function and before the line:
throw new MissingScopeException($scopes); add your custom error response.
Example with code:
public function handle($request, $next, ...$scopes)
{
if (! $request->user() || ! $request->user()->token()) {
throw new AuthenticationException;
}
foreach ($scopes as $scope) {
if ($request->user()->tokenCan($scope)) {
return $next($request);
}
}
// Custom error response
return response()->json(['success' => false, 'errors' => ['This type of user cannot do this action.'], 'data' => null]);
// This line can be commented or deleted.
throw new MissingScopeException($scopes);
}

Laravel 5.7 ModelNotFoundException not return json for API calls

I want to return a json response when an api call is made to a laravel 5.7 app api route when the model is not found. To do this I have modified the render() method of app\Exceptions\Handler.php like this
public function render($request, Exception $exception)
{
if ($exception instanceof ModelNotFoundException && $request->wantsJson()) {
return response()->json(['message' => 'Not Found!'], 404);
}
return parent::render($request, $exception);
}
and my controller show() method is using a Book model like this
public function show(Book $book)
{
return new BookResource($book->load('ratings'));
}
Test on postman, a get call to localhost:8000/api/books/1 (id 1 has been deleted) keeps returning the default laravel 404 not found page instead of json.
Have I missed a step or something? I also noticed that adding a conditional statement inside the controller show() method like this
public function show(Book $book)
{
if ($book) {
return new BookResource($book->load('ratings'));
} else {
return response()->json(['message' => 'Not found'], 404);
}
}
returns the same html result instead of json.
What will be the proper way to handle this scenario?
Your code is correct. The problem is that you are probably testing it on a Local environment so in your .env you have set:
APP_DEBUG=true, switch it to APP_DEBUG=false and you will see your custom message.
PS: $request->wantsJson() is not necessary if your clients send the correct header info, eg: 'accept:application/json'
You can remove $request->wantsJson
or you can set the header in your request "Accept" => "application/json"
May this can help you:
public function render($request, Exception $exception)
{
if ($exception instanceof ModelNotFoundException && ($request->wantsJson() || $request->ajax())) {
return response()->json(['message' => 'Not Found!'], 404);
}
return parent::render($request, $exception);
}

Slim3/DRY - How to handle errors/exceptions correctly without duplicating code?

I'm working on a fairly large JSON API using Slim3. My controllers/actions are currently littered with the following:
return $response->withJson([
'status' => 'error',
'data' => null,
'message' => 'Username or password was incorrect'
]);
At certain points in the application anything can go wrong and the response needs to be appropriate. But one thing that is common is the error responses are always the same. The status is always error, the data is optional (in the case of form validation errors data will contain those) and message is set to indicate to the user or consumer of the API what went wrong.
I smell code duplication. How can I reduce the code duplication?
From the top of my head all I could think of doing was creating a custom exception, something like App\Exceptions\AppException that takes option data and the message will be obtained form $e->getMessage().
<?php
namespace App\Exceptions;
class AppException extends Exception
{
private $data;
public function __construct($message, $data = null, $code = 0, $previous = null)
{
$this->data = $data;
parent::__construct($message, $code, $previous);
}
public function getData()
{
return $this->data;
}
}
Following that create middleware that calls $next wrapped in a try/catch:
$app->add(function($request, $response, $next) {
try {
return $next($request, $response);
}
catch(\App\Exceptions\AppException $e)
{
$container->Logger->addCritical('Application Error: ' . $e->getMessage());
return $response->withJson([
'status' => 'error',
'data' => $e->getData(),
'message' => $e->getMessage()
]);
}
catch(\Exception $e)
{
$container->Logger->addCritical('Unhandled Exception: ' . $e->getMessage());
$container->SMSService->send(getenv('ADMIN_MOBILE'), "Shit has hit the fan! Run to your computer and check the error logs. Beep. Boop.");
return $response->withJson([
'status' => 'error',
'data' => null,
'message' => 'It is not possible to perform this action right now'
]);
}
});
Now all I have to do at points in the code is to throw new \App\Exceptions\AppException("Username or password incorrect", null).
My only issue with this is it feels like I'm using exceptions for the wrong reasons and it may make debugging a little more difficult.
Any suggestions on reducing the duplicates and cleaning up error responses?
You can achieve similar similar results by creating an error handler which outputs JSON.
namespace Slim\Handlers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
final class ApiError extends \Slim\Handlers\Error
{
public function __invoke(Request $request, Response $response, \Exception $exception)
{
$status = $exception->getCode() ?: 500;
$data = [
"status" => "error",
"message" => $exception->getMessage(),
];
$body = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
return $response
->withStatus($status)
->withHeader("Content-type", "application/json")
->write($body);
}
}
You must also configure Slim to use your custom error handler.
$container = $app->getContainer();
$container["errorHandler"] = function ($container) {
return new Slim\Handlers\ApiError;
};
Check Slim API Skeleton for example implemention.

Laravel 5 : How to use findOrFail() method?

I just follow some tutorial and so far what I do is :
my App/Exceptions/Handler.php
<?php
...
use Illuminate\Database\Eloquent\ModelNotFoundException;
...
public function render($request, Exception $e)
{
if ($e instanceof ModelNotFoundException){
abort(404);
}
return parent::render($request, $e);
}
and my UsersController looks like this :
...
public function edit($id)
{
$data = User::findOrFail($id);
$roles = Role::where('title', '!=', 'Super Admin')->get();
return View('admin.user.edit', compact(['data', 'roles']));
}
...
with the above code if I visit http://my.url/users/10/edit I get NotFoundHttpException in Application.php line 901:, yes because there is no id 10 in my record, but with User::find($id); I get normal view without data, since no id 10 in my record.
What I want is show default 404 then redirect to somewhere or return something if record not found with User::findOrFail($id); ? How I can do that ?
Thanks, any help appreciated.
ps: .env APP_DEBUG = true
This does what you asked. No need for exceptions.
public function edit($id)
{
$data = User::find($id);
if ($data == null) {
// User not found, show 404 or whatever you want to do
// example:
return View('admin.user.notFound', [], 404);
} else {
$roles = Role::where('title', '!=', 'Super Admin')->get();
return View('admin.user.edit', compact(['data', 'roles']));
}
}
Your exception handler is not necessary as it is. Regarding Illuminate\Database\Eloquent\ModelNotFoundException:
If the exception is not caught, a 404 HTTP response is automatically sent back to the user, so it is not necessary to write explicit checks to return 404 responses when using [findOrFail()].
Also, I'm pretty sure you get the exception page instead of 404 now because you're in debug mode.
public function singleUser($id)
{
try {
$user= User::FindOrFail($id);
return response()->json(['user'=>user], 200);
} catch (\Exception $e) {
return response()->json(['message'=>'user not found!'], 404);
}
}
findOrFail() is alike of find() function with one extra ability - to throws the Not Found Exceptions
Sometimes you may wish to throw an exception if a model is not found. This is particularly useful in routes or controllers. The findOrFail and firstOrFail methods will retrieve the first result of the query; however, if no result is found, a Illuminate\Database\Eloquent\ModelNotFoundException will be thrown:
$model = App\Flight::findOrFail(1);
$model = App\Flight::where('legs', '>', 100)->firstOrFail();
If the exception is not caught, a 404 HTTP response is automatically sent back to the user. It is not necessary to write explicit checks to return 404 responses when using these methods:
Route::get('/api/flights/{id}', function ($id) {
return App\Flight::findOrFail($id);
});
Its not recommended but If still you want to handle this exception globally, following are the changes as per your handle.php
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
if ($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
//redirect to errors.custom view page
return response()->view('errors.custom', [], 404);
}
return parent::render($request, $exception);
}
Late addition to above topic: If you want to handle the exception for an API backend and you don't want to make the check for an empty result in each method and return a 400 Bad request error individually like this...
public function open($ingredient_id){
$ingredient = Ingredient::find($ingredient_id);
if(!$ingredient){
return response()->json(['error' => 1, 'message' => 'Unable to find Ingredient with ID '. $ingredient_id], 400);
}
return $ingredient;
}
Instead use findOrFailand catch exception in app/Exceptions/Handler.php.
public function render($request, Exception $exception)
{
if ($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
return response()->json(['error'=>1,'message'=> 'ModelNotFoundException handled for API' ], 400);
}
return parent::render($request, $exception);
}
This will then look like this in your Controllers:
public function open($ingredient_id){
return Ingredient::findOrFail($ingredient_id);
}
which is much cleaner. Consider that you have plenty of Models and plenty of Controllers.

Categories