Exceptions will not render Laravel 5.7 - laravel

I am using Laravel v 5.7.15.
I wrote a validation helper which validates an API request - this works successfully, and prior I was using a try/catch to surround it.
I have moved on to handling the exception in the handler, however I cannot get the function 'render' to run - it goes straight into 'report' and throws in the exception in my tinker console.
Handler: (full class as requested)
<?php
namespace App\Exceptions;
use Illuminate\Validation\ValidationException;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use App\Log;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* #var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* #var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* #param Exception $exception
* #return mixed|void
* #throws Exception
*/
public function report(Exception $exception)
{
parent::report($exception);
}
/**
* 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)
{
dd($exception);
$log = new Log();
$log->status = 2;
// Validate API incoming data
if ($exception instanceOf ValidationException) {
foreach ($exception->errors() as $error) {
// collect multiple validation errors
$message[] = implode('', $error);
}
$message = implode('', $message);
$log->message = $message;
$log->save();
$response = [
'message' => $message,
'status' => 400,
];
} else {
$response = [
'message' => $exception->getMessage(),
'status' => '500',
];
}
return parent::render($request, $exception);
}
}
This fails to die and dump, however I can dd in the report function and this works fine. The rest of this file has been left untouched, save for the includes at the top of the file.
This is how I call my validator in my controller:
$this->validate($request, BlueparkValidatorArrays::$getOrders);
If anybody could point me in the right direction, I would be most grateful.

This may be caused by a problem in your log configuration.
The call to parent::report($exception); runs the following from the laravel source code:
public function report(Exception $e)
{
...
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $e; // throw the original exception
}
...
}
Note throw $e not throw $ex. So if creating the logger implementation fails, the original exception that was being processed is thrown.
To test this, comment out parent::report($exception); in your report function and see if render() is called as expected.
If it is, your log configuration is not working. Make sure you have the correct permissions on your log location and that your .env file doesn't override any of laravel's logging settings. See How to debug Laravel error 500 with no logs, no information

Related

laravel return exception as json instead of an html

I have an old Laravel app that is upgraded to v7. Currently exceptions are returned as an HTML page. But I would like them to be returned as json response. I understand that exceptions are handled in Exceptions\Handler.php:
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* #var array<int, class-string<Throwable>>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* #var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* #return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
});
}
}
How can I return the whole exception sack as json?
Edit: The app is old and was using a custom exception handler. Now after the upgrade to V.7 it would be nice to use the default exception handler so that exceptions are returned in json.
Edit 2:
When I use
return response()->json([
'exception' =>exception,
]);
I get an empty object. I don't want to only return the message of the exception but the whole exception stack.
You can override render function inside App\Exceptions\Handler.php
and you can read more about it https://laravel.com/docs/7.x/errors#render-method

Laravel get middleware for current route

How do you retrieve the middlewares for the current route?
I'm trying to set the exception handler to work differently based on if you're within a particular part of the website by checking if a middleware has been added to the route.
<?php
namespace App\Exceptions;
//use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Abrigham\LaravelEmailExceptions\Exceptions\EmailHandler as ExceptionHandler;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use Illuminate\Support\Facades\Route;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* #var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* #var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* #param \Throwable $exception
* #return void
*
* #throws \Exception
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Throwable $exception
* #return \Symfony\Component\HttpFoundation\Response
*
* #throws \Throwable
*/
public function render($request, Throwable $exception)
{
switch(true) {
case $exception instanceof \Illuminate\Session\TokenMismatchException:
// Redirect back if form invalid
return redirect()
->back()
->withInput($request->except($this->dontFlash))
->withErrors('The form has expired due to inactivity. Please try again');
break;
case $exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException:
// Redirect back with message if model not found error
$redirect = app('redirect');
// Check for different url to prevent redirect loop
if (request()->fullUrl() === $redirect->back()->getTargetUrl()){
// $currentRouteMiddleware = request()->route()->controllerMiddleware() returns empty array
// $currentRouteMiddleware = Route::current()->router->getMiddleware(); router is private
$response = $redirect->to(isset($currentRouteMiddleware['admin.user']) ? '/admin' : '/');
} else {
$response = $redirect->back();
}
return $response
->withInput($request->except($this->dontFlash))
->withErrors('That page could not be found. Please try again or report the broken link: ' . $request->getRequestUri());
break;
}
return parent::render($request, $exception);
}
}
If I dump the current route inside the router it shows the middleware array that I need to check against: dd(Route::current()) but there doesn't appear to be a way of accessing the current router e.g: $currentRouteMiddleware = Route::current()->router->getMiddleware();
There are a couple options based on what you're looking for.
If you want all the middleware aliases assigned to the route, you can use:
Route::current()->gatherMiddleware();
This does not expand assigned middleware groups, so the result from this might look something like:
[
'web',
'admin.user'
]
If you want all of the middleware classes assigned to the route, you can use:
Route::gatherRouteMiddleware(Route::current());
This will give you the classes, but it will not have the associated aliases or groups for the classes. The result from this might look like:
[
"App\Http\Middleware\EncryptCookies",
"Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse",
"Illuminate\Session\Middleware\StartSession",
"Illuminate\View\Middleware\ShareErrorsFromSession",
"App\Http\Middleware\VerifyCsrfToken",
"Illuminate\Routing\Middleware\SubstituteBindings",
"TCG\Voyager\Http\Middleware\VoyagerAdminMiddleware"
]
The Route::getMiddleware() in the Handler is simply a list of \App\Http\Kernel::$routeMiddleware.
dd(Route::getMiddleware());
array:7 [▼
"auth" => "App\Http\Middleware\Authenticate"
"auth.basic" => "Illuminate\Auth\Middleware\AuthenticateWithBasicAuth"
"guest" => "App\Http\Middleware\RedirectIfAuthenticated"
"admin" => "App\Http\Middleware\AdminMiddleware"
// ...
]
It is recommended to declare a constant (or global variable) and check whether the corresponding constant is declared in the Handler to check whether AdminMiddleware has passed.
// app/Http/Middleware/AdminMiddleware.php
public function handle($request, Closure $next)
{
// if ... return ...
define('__APP_IS_ADMIN', true);
return $next($request);
}
// app/Exceptions/Handler.php
public function render($request, Exception $e)
{
// ...
$response = $redirect->to(defined('__APP_IS_ADMIN') ? '/admin' : '/');
request()->route()->controllerMiddleware();
List middlewares applied in the current route inside App\Exceptions\Handler. Using in unauthenticated method.
/**
* Convert an authentication exception into a response.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Auth\AuthenticationException $exception
* #return \Symfony\Component\HttpFoundation\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
return $request->expectsJson()
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest(in_array('admin', $request->route()->middleware(), true) ? route('admin.login') : route('login'));
}
I used this to get the "admin" middleware and redirect the user to the correct login page.

Automatic dependency injection handle exception laravel

Here is my case
I have cron job(console command)
/**
* Execute the console command.
*
* #return mixed
*/
public function handle(OrdersMagentoService $magentoService)
{
try {
$orders = $magentoService->getRemoteOrders();
print_r($orders);
} catch (SoapFault $e) {
$this->line('Error connect to soap api, error: ' . $e->getMessage());
die;
} catch (\Throwable | \Exception $e) {
print_r($e->getMessage());
die;
}
}
In handle method i automaticaly inject OrdersMagentoService, this service do connect to magento soap api and exctend from BaseMagentoService
class OrdersMagentoService extends BaseMagentoService
{
public function getRemoteOrders()
{
$complex = [
'complex_filter' => [
[
'key' => 'status',
'value' =>
[
'key' => 'in',
'value' => 'closed,canceled,holded,processing,complete'
]
],
[
'key' => 'updated_at',
'value' => [
'key' => 'from',
'value' => now()->subDays(200),
]
]
]
];
return $this->salesOrderList($complex);
}
}
class BaseMagentoService
{
/**
* #var
*/
private $client;
/**
* #var
*/
private $session;
/**
* #var \Illuminate\Config\Repository|mixed
*/
protected $config;
/**
* BaseMagentoService constructor.
*/
public function __construct()
{
$this->config = config('services.magento');
$this->connect();
}
/**
* Do connect to soap api v2
*
* #throws \SoapFault
*/
private function connect()
{
$this->client = new SoapClient($this->getApiUrl());
$this->session = $this->client->login($this->config['user_name'], $this->config['password']);
}
public function __call($resource, $arguments)
{
return $this->client->$resource($this->session, ...$arguments);
}
}
In BaseMagentoService constructor i create connection to magento soap api. But if connection throw error(for example wrong username and pass) then i can't handle this in cron job file. I understand that laravel at first create OrdersMagentoService, it throw error and try catch in handle function not works, but i dont know how to fix this.
I can add in handle method
try {
$magentoService = resolve(OrdersMagentoService::class)
$orders = $magentoService->getRemoteOrders();
print_r($orders);
} catch (SoapFault $e) {
$this->line('Error connect to soap api, error: ' . $e->getMessage());
die;
} catch (\Throwable | \Exception $e) {
print_r($e->getMessage());
die;
}
And remove automatic DI, and tit should work well, but i dont want to do it.
Also if i add some try catch in connect method of BaseMagentoService then i cant log this error in my cron job.
What is best way to handle this?
First I would switch the handling from the artisan command to a Job or an Event that might be more suited to the problem. Then your artisan command simply fires off this new job or event each time is executed.
This is also somehow stated in the docs as a best practice:
For greater code reuse, it is good practice to keep your console commands light and let them defer to application services to accomplish their tasks.
Then if you look at the documentation about jobs, there is a section about error handling for failed jobs where you are told that you can define a method that gets triggered once he job has failed. Example excerpt taken from the docs:
class YourJob implements ShouldQueue
{
/**
* Execute the job.
*
* #param AudioProcessor $processor
* #return void
*/
public function handle(AudioProcessor $processor)
{
// Process uploaded podcast...
}
/**
* The job failed to process.
*
* #param Exception $exception
* #return void
*/
public function failed(Exception $exception)
{
// Send user notification of failure, etc...
}
}
You can read more in the documentation. There is also a paragraph about global jobs failure handling that might suit your use case

Laravel API returns a view 404 error instead of JSON error

I am trying to create a RESTful API using laravel, I'm trying to fetch a resource with an invalid ID, and the result is 404 since it is not found, but my problem is the response is not in JSON format, but a View 404 (by default) with HTML. Is there any way to convert the response into JSON? For this situation, I use Homestead.
I try to include a fallback route, but it does not seem to fit this case.
Route::fallback(function () {
return response()->json(['message' => 'Not Found.'], 404);
});
I try to modify the Handler (App\Exceptions), but nothing change.
public function render($request, Exception $e)
{
if ($e instanceof ModelNotFoundException) {
if ($request->ajax()) {
return response()->toJson([
'message' => 'Not Found.',
], 404);
}
}
return parent::render($request, $e);
}
For Laravel 9
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Register the exception handling callbacks for the application.
*
* #return void
*/
public function register()
{
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});
}
You'll need to send the correct Accept header in your request: 'Accept':'application/json'.
Then Illuminate\Foundation\Exceptions\Handler will care of the formatting in the render method in your response:
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
if your project is only a RESTful API and no views, you could add a new middleware which add ['accept' => 'application/json'] header to all request. this will ensure that all response will return a json, instead of the views
<?php
namespace App\Http\Middleware;
use Closure;
class AddAjaxHeader
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$request->headers->add(['accept' => 'application/json']);
return $next($request);
}
}
and add it into Kernel.php
You need to set APP_DEBUG in you .env file to false.
or, if you use a phpunit, like follows
config()->set('app.debug', false);
In Laravel 9, as per Official Documentation 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.
so, we need to checking Previous Exception using $e->getPrevious() just check previous exception is instanceof ModelNotFoundException or not
see below my code
// app/Exceptions/Handler.php file
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*')) {
if ($e->getPrevious() instanceof ModelNotFoundException) {
/** #var ModelNotFoundException $modelNotFound */
$modelNotFound = $e->getPrevious();
if($modelNotFound->getModel() === Product::class) {
return response()->json([
'message' => 'Product not found.'
], 404);
}
}
return response()->json([
'message' => 'not found.'
], 404);
}
});
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Throwable $exception
* #return \Symfony\Component\HttpFoundation\Response
*
* #throws \Throwable
*/
public function render($request, Throwable $exception)
{
switch (class_basename($exception)) {
case 'NotFoundHttpException':
case 'ModelNotFoundException':
$exception = new NotFoundHttpException('Not found');
break;
}
return parent::render($request, $exception);
}

Laravel 5.7 How to Log 404 With URL

I want to log 404 errors in Laravel 5.7, but I don't understand how to turn this on. Additional to logging 404 errors, I'd like to log the URL that was requested. Other errors are logged correctly.
.env
APP_DEBUG=true
LOG_CHANNEL=stack
config/logging.php
'stack' => [
'driver' => 'stack',
'channels' => ['daily'],
],
Per the Error Handling documentation:
The $dontReport property of the exception handler contains an array of
exception types that will not be logged. For example, exceptions
resulting from 404 errors, as well as several other types of errors,
are not written to your log files. You may add other exception types
to this array as needed:
In app/Exceptions/Handler the $dontReport array is empty.
I have customized the 404 view by having a Blade file resources/views/errors/404.blade.php
Based on this answer I've tried this code in app/Exceptions/Handler, but nothing shows up in the logs:
public function report(Exception $exception)
{
if ($this->isHttpException($exception)) {
if ($exception instanceof NotFoundHttpException) {
Log::warning($message);
return response()->view('error.404', [], 404);
}
return $this->renderHttpException($exception);
}
parent::report($exception);
}
UPDATE after accepting Mozammil's answer which works fine.
I've shortened his answer to the below. Don't forget to add use Illuminate\Support\Facades\Log to the Handler file.
public function render($request, Exception $exception)
{
if ($exception instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
Log::warning('404: ' . $request->url());
return response()->view('errors.404', [], 404);
}
return parent::render($request, $exception);
}
I have a similar requirement. Here's how I achieved it.
I have a helper method to determine if it's a 404.
private function is404($exception)
{
return $exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException
|| $exception instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
}
I also have another method to actually log the 404.
private function log404($request)
{
$error = [
'url' => $request->url(),
'method' => $request->method(),
'data' => $request->all(),
];
$message = '404: ' . $error['url'] . "\n" . json_encode($error, JSON_PRETTY_PRINT);
Log::debug($message);
}
Then, to log the error, I just do something like this in the render() method:
public function render($request, Exception $exception)
{
if($this->is404($exception)) {
$this->log404($request);
}
return parent::render($request, $exception);
}
I didn't know about the $internalDontReport. However, in all cases, my implementation worked for me :)
I use Telescope
Laravel Telescope
Laravel Telescope is an elegant debug assistant for the Laravel framework. Telescope provides insight into the requests coming into your application, exceptions, log entries, database queries, queued jobs, mail, notifications, cache operations, scheduled tasks, variable dumps and more.
https://laravel.com/docs/5.7/telescope
I attend to catch all kind of 4xx errors so, I edit in the app/Exceptions/Handler.php file by adding the below code in the render function
if($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException ||
$exception instanceof \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException ||
$exception instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException){
$error = [
'message'=> $exception->getMessage(),
'type' => \get_class($exception),
'url' => $request->url(),
'method' => $request->method(),
'data' => $request->all(),
];
$message = $exception->getStatusCode().' : ' . $error['url'] . "\n" . \json_encode($error, JSON_PRETTY_PRINT);
//Store the object in DB or log file
\Log::debug($message);
}
this Code will catch Exception for [ModelNotFoundException, MethodNotAllowedHttpException, NotFoundHttpException] - in short words this will catch 404 error, model not found in DB and bad method - and create an object called $error and you will be able to store it in what ever you want.
So the app/Exceptions/Handler.php will be like
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* #var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* #var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* #param \Throwable $exception
* #return void
*
* #throws \Exception
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Throwable $exception
* #return \Symfony\Component\HttpFoundation\Response
*
* #throws \Throwable
*/
public function render($request, Throwable $exception)
{
if($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException ||
$exception instanceof \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException ||
$exception instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException){
$error = [
'message'=> $exception->getMessage(),
'type' => \get_class($exception),
'url' => $request->url(),
'method' => $request->method(),
'data' => $request->all(),
];
$message = $exception->getStatusCode().' : ' . $error['url'] . "\n" . \json_encode($error, JSON_PRETTY_PRINT);
\Log::debug($message);
}
return parent::render($request, $exception);
}
}
P.S. I am using laravel 8 but I am sure it will work in most popular versions.

Resources