get current request in 404 page - laravel

Hey I want to use the current request object as the facade not the static way($request not Request::) in a custom 404 blade file.
I don't know if I can hint about it to the error handler or is there a way to create that object?
Should/Could I do it via the Expections/Handler.php file?
I've found Here the following answer:
//Create a view and set this code in app/Exception/Handler.php :
/**
* Render an exception into a response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if($e instanceof NotFoundHttpException)
{
return response()->view('missing', [], 404);
}
return parent::render($request, $e);
}
//Set this use to get it working :
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Is this the right way to do it?

Yes, you can do it from the Handler. Inside the render() method:
if ($e instanceof NotFoundHttpException) {
return response()->view('your.view.name', $dataYouWantToPass);
}

Related

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.

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);
}

How to return unauthorized using Laravel API Authentication

I am using Laravel API authentication with a token. (as explained here:
https://laravel.com/docs/5.8/api-authentication#protecting-routes)
I am running some tests with Postman and it works fine. When I try to access the route without a valid token, I see that the response is the (html of the) login page of my app. How can I return a Unauthorized message instead of the complete login page? Do I have to create a custom middleware?
Controller
class ExampleController extends Controller
{
public function __construct()
{
$this->middleware('auth:api');
}
public function show(Request $request) {
return response()->json($request->user()->name);
}
}
Please add the method in the class Handler in the file location app/Exceptions/Handler.php
/**
* Convert an authentication exception into an unauthenticated response.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Auth\AuthenticationException $exception
* #return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest(route('login'));
}
And also add the following line above the class in the same file as mentioned above:
use Illuminate\Auth\AuthenticationException;
In the postman within the headers section please add the following header :
X-Requested-With:XMLHttpRequest
Hope this helps and resolves the issue. Thanks.
I am using Laravel 8 with passport.
In my case I had to add an "unauthenticated" function inside app/Http/Middleware/Authenticate like so:
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* #param \Illuminate\Http\Request $request
* #return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
// Add new method
protected function unauthenticated($request, array $guards)
{
abort(response()->json(
[
'api_status' => '401',
'message' => 'UnAuthenticated',
], 401));
}
}
Be sure to be sending the right headers in your request
Content-Type: application/json
Laravel 8 update:
default handler already handle this scenario
File: \Illuminate\Foundation\Exceptions\Handler.php
/**
* 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($exception->redirectTo() ?? route('login'));
}
This is how Laravel do expectsJson in \Illuminate\Http\Concerns\InteractsWithContentTypes.php
/**
* Determine if the current request probably expects a JSON response.
*
* #return bool
*/
public function expectsJson()
{
return ($this->ajax() && ! $this->pjax() && $this->acceptsAnyContentType()) || $this->wantsJson();
}
/**
* Determine if the current request is asking for JSON.
*
* #return bool
*/
public function wantsJson()
{
$acceptable = $this->getAcceptableContentTypes();
return isset($acceptable[0]) && Str::contains($acceptable[0], ['/json', '+json']);
}
So note that content type header of request is useless. The header useful is "Accept: application/json"
Make sure you are sending correct headers
Accept application/json
Also, you can create your own Middleware for this.

Override exception in handler

I am using Route model binding using the id of a product and my route looks like this:
Route::get('product/{product}', ProductController#index');
I also have a custom ProductNotFoundException which I want to return when the route model binding only for this model fails to get the row (the row does not exist or is soft deleted.)
Any ideas how I could achieve that?
The two solutions I thought of are:
Remove Route Model binding (obviously :P)
Override the exception in the Exceptions/Handler
I chose to go with the second and do the following
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
*
* #return \Symfony\Component\HttpFoundation\Response
* #throws ProductNotFoundException
*/
public function render($request, Exception $exception)
{
if ($exception instanceof ModelNotFoundException && $exception->getModel() === 'App\Models\Product') {
throw new ProductNotFoundException();
}
return parent::render($request, $exception);
}
However, I am not sure whether is a good practice to throw an exception in the exception handler.
Can anyone see any other way or advice of the above solution?
Customize the binding resolution in RouteServiceProvider:
Route::bind('product', function ($value) {
$product = Product::find($value);
if ($product === null) {
throw new ProductNotFoundException();
}
return $product;
});
Explicit Model Binding

Laravel - 404 error handling for different route groups

I want to show two types of 404 error screen for users, authenticated users with admin rights inside the route /admin see an error page style and unauthenticated guests in the route/see another error page, how can I do this?
You can use the Exception Handler's render() method. From the documentation,
The render method is responsible for converting a given exception into
an HTTP response that should be sent back to the browser.
Instead of returning same views for all users, you can add the authorization logic in the App\Exceptions\Handler calss:
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if ($e instanceof CustomException) {
if(isAdmin()) {
return response()->view('admin.errors.custom', [], 500);
}
return response()->view('errors.custom', [], 500);
}
return parent::render($request, $e);
}

Resources