I'm trying to change the message from 500 errors if certain conditions are met. I have this middleware in a file called ApiAfter.php:
<?php
namespace App\Http\Middleware;
use App\Helpers\Helper;
use App\Helpers\LogHelper;
use Carbon\Carbon;
use Closure;
use DateTime;
use Exception;
use Illuminate\Http\Response;
class ApiAfter
{
public function handle($request, Closure $next) {
return $next($request);
}
public function terminate($request, $response) {
// move everything in handle function to this
// logging the results of the request
$response = $this->fixFiveHundred($request, $response);
// I do some other stuff here
return $response;
}
private function fixFiveHundred($request, $response) {
if ($response->status() !== 500) return $response;
try {
if (!empty($response->original['message']) && $response->original['message'] === "Server Error") {
if (!empty($response->exception)) {
$newMessage = $response->exception->getMessage();
return response($newMessage, 500); // this is the line of code I'm having trouble with
}
}
} catch(Exception $e) {
return $response;
}
return $response;
}
}
On the line that says return response($newMessage, 500);, I've tried lots of different things but nothing is actually changing the response. Can you not change a response in the Terminate function?
You should avoid handling exception in a custom middleware. Laravel provides a simple class that allows simple customization. Have a look to "render method" of Laravel Error Handling in official docs.
Can you not change a response in the Terminate function?
Short answer: No.
The terminate function in a middleware runs after the response was sent to the browser. Thus, you can't modify the response.
As #Roberto Ferro pointed, the right place to handle custom response for exceptions is using the render in the Exception Handler
Related
I've been testing the new Slim 4 framework and redirects work fine for me in normal classes, but I cannot seem to get them working in middleware, where a response is dynamically generated (apparently?) by the Request Handler. When I try to redirect with a Location header, it simply fails to redirect, and my route continues to the original location.
Here’s a basic version of my authentication middleware for testing:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): Response {
$response = $handler->handle($request);
$loggedInTest = false;
if ($loggedInTest) {
echo "User authorized.";
return $response;
} else {
echo "User NOT authorized.";
return $response->withHeader('Location', '/users/login')->withStatus(302);
}
}
}
Has anybody got this to work? And if so, how did you accomplish it? Thanks in advance.
I think I see the problem with this code.
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): Response {
$response = $handler->handle($request);
$loggedInTest = false;
if ($loggedInTest) {
echo "User authorized.";
return $response;
} else {
echo "User NOT authorized.";
return $response->withHeader('Location', '/users/login')->withStatus(302);
}
}
}
When you call $handler->handle($request), that processes the request normally and calls whatever closure is supposed to handle the route. The response hasn't been completed yet, you can still append stuff to it, but the headers are already set, so you can't do a redirect, because the headers are done.
Maybe try this:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface {
$loggedInTest = false;
if ($loggedInTest) {
$response = $handler->handle($request);
echo "User authorized.";
return $response;
} else {
$response = new Response();
// echo "User NOT authorized.";
return $response->withHeader('Location', '/users/login')->withStatus(302);
}
}
}
If the login test fails, we never call $handler->handle(), so the normal response doesn't get generated. Meanwhile, we create a new response.
Note that the ResponseInterface and Response can't both be called Response in the same file, so I had to remove that alias, and just call the ResponseInterface by its true name. You could give it a different alias, but I think that would only create more confusion.
Also, I commented out the echo before the redirect. I think this echo will force headers to be sent automatically, which will break the redirect. Unless Slim 4 is doing output buffering, in which case you're still not going to see it, because the redirect will immediately send you to a different page. Anyway, I commented it out to give the code the best chance of working but left it in place for reference.
Anyway, I think if you make that little change, everything will work. Of course, this post is almost a year old, so you've probably solved this on your own, switched to F3, or abandoned the project by now. But hopefully, this will be helpful to someone else. That's the whole point of StackOverflow, right?
eimajenthat is right, except that you cannot create an instance of interface.
Try this instead:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): Response {
global $app; // Assuming $app is your global object
$loggedInTest = false;
if ($loggedInTest) {
$response = $handler->handle($request);
echo "User authorized.";
return $response;
} else {
$response = $app->getResponseFactory()->createResponse();
// echo "User NOT authorized.";
return $response->withHeader('Location', '/users/login')->withStatus(302);
}
}
}
I was growing so frustrated by Slim 4 and redirect issues that I took a look at FatFreeFramework and had the exact same problem. So I knew it was something I was doing. My code was putting the app into a never-ending redirect loop. I can make it work by validating the redirect URL like so in FatFreeFramework:
class Controller {
protected $f3;
public function __construct() {
$isLoggedIn = false;
$this->f3 = Base::instance();
if ($isLoggedIn == false && $_SERVER['REQUEST_URI'] != '/login') {
$this->f3->reroute('/login');
exit();
}
}
}
Therefore, although I haven't actually taken the time to test it, I'm assuming I could fix it in Slim 4 by doing something like:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): Response {
$response = $handler->handle($request);
$loggedInTest = false;
if (!$loggedInTest && $_SERVER['REQUEST_URI'] != '/user/login') {
return return $response->withHeader('Location', '/users/login')->withStatus(302);
} else {
return $response;
}
}
}
Does anybody have another idea for how to break a continuous redirect loop? Or is the $_SERVER variable the best option?
Thanks in advance.
Use 2 response
namespace App\middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response as Response7;
use Psr\Http\Message\ResponseInterface as Response;
final class OtorisasiAdmin {
public function __invoke(Request $request, RequestHandler $handler): Response {
$session = new \Classes\session();
$session->start();
$isAdmin=($session->has("login","admin"))?true:false;
if(!$isAdmin){
$response = new Response7();
$error = file_get_contents(__dir__."/../../src/error/404.html");
$response->getBody()->write($error);
return $response->withStatus(404);
}
$response=$handler->handle($request);
return $response;
}
}
Info: All my routes look like this /locale/something for example /en/home
works fine.
In my controller I'm using the firstOrFail() function.
When the fail part is triggered the function tries to send me to /home.
Which doesn't work because it needs to be /en/home.
So how can I adjust the firstOrFail() function to send me to /locale/home ?
What needs to changed ?
You can treat it in several ways.
Specific approach
You could surround your query with a try-catch wherever you want to redirect to a specific view every time a record isn't found:
class MyCoolController extends Controller {
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Redirect;
//
function myCoolFunction() {
try
{
$object = MyModel::where('column', 'value')->firstOrFail();
}
catch (ModelNotFoundException $e)
{
return Redirect::to('my_view');
// you could also:
// return redirect()->route('home');
}
// the rest of your code..
}
}
The only downside of this is that you need to handle this everywhere you want to use the firstOrFail() method.
The global way
As in the comments suggested, you could define it in the Global Exception Handler:
app/Exceptions/Handler.php
# app/Exceptions/Handler.php
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Redirect;
// some code..
public function render($request, Exception $exception)
{
if ($exception instanceof ModelNotFoundException && ! $request->expectsJson())
{
return Redirect::to('my_view');
}
return parent::render($request, $exception);
}
I am learning laravel and I have just created a middleware with a very little session work. but I am getting below error:
FatalThrowableError in VerifyCsrfToken.php line 136: Call to a member
function setCookie() on null
Here is the middleware:
<?php
namespace App\Http\Middleware;
use Closure;
class Adminlogin {
public function handle($request, Closure $next) {
echo 1;
if (!$request->session()->has('userid')) {
return view('admin.auth.login');
}
return $next($request);
}
}
In this scenario response()->view() returns the specified view with the status code 200, but you are able to modify the response in many ways.
From the documentation: If you need control over the response's status and headers but also need to return a view as the response's content, you should use the view method: https://laravel.com/docs/5.3/responses
return response()
->view('hello', $data, 200)
->header('Content-Type', $type);
You shouldn't return a view from your middleware. Instead, try redirecting to a route that returns that view.
Like -
return redirect()->route('login');
I am trying to develop a RESTful API with Laravel 5.2. I am stumbled on how to return failed authorization in JSON format. Currently, it is throwing the 403 page error instead of JSON.
Controller: TenantController.php
class TenantController extends Controller
{
public function show($id)
{
$tenant = Tenant::find($id);
if($tenant == null) return response()->json(['error' => "Invalid tenant ID."],400);
$this->authorize('show',$tenant);
return $tenant;
}
}
Policy: TenantPolicy.php
class TenantPolicy
{
use HandlesAuthorization;
public function show(User $user, Tenant $tenant)
{
$users = $tenant->users();
return $tenant->users->contains($user->id);
}
}
The authorization is currently working fine but it is showing up a 403 forbidden page instead of returning json error. Is it possible to return it as JSON for the 403? And, is it possible to make it global for all failed authorizations (not just in this controller)?
We managed to resolve this by modifying the exceptions handler found in App\Exceptions\Handler.php adding it in the render function.
public function render($request, Exception $e)
{
if ($e instanceof AuthorizationException)
{
return response()->json(['error' => 'Not authorized.'],403);
}
return parent::render($request, $e);
}
Yes, make a simple before method in your policy which will be executed prior to all other authorization checks,
public function before($user, $ability,Request $request)
{
if (!yourconditiontrue) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
return abort('403');
}
}
}
You can intercept the exception
try {
$this->authorize('update', $data);
} catch (\Exception $e)
{
return response()->json(null, 403);
}
As for the latest version of Laravel, as of now version >=7.x,
Generally setting request headers 'Accept' => 'application/json' will tell Laravel that you expect a json response back.
For errors you need to also turn off debugging by setting the APP_DEBUG=false on your .env file, which will make sure the response is json and no stacktrace is provided.
The accepted answer works, but if you don't want to return json for every route you can handle this with middleware.
A brief outline of how to do this:
Create an ApiAuthorization class and extend your main auth class.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Access\AuthorizationException;
class ApiAuthorization extends Authorize
{
public function handle($request, Closure $next, $ability, ...$models)
{
try {
$this->auth->authenticate();
$this->gate->authorize($ability, $this->getGateArguments($request, $models));
} catch (AuthorizationException $e) {
return response()->json(['error' => 'Not authorized.'],403);
}
return $next($request);
}
}
Add the middleware to $routeMiddleware in App\Http\Kernel.php
'api.can' => \App\Http\Middleware\ApiAuthorization::class,
Update your route. You can now use your new api auth middleware by calling api.can similar to the example in the docs
Route::get('tenant', [
'as' => 'api.tenant',
'uses' => 'TenantController#show'
])->middleware('api.can:show,tenant');
This method allows you to return json for specific routes without modifying the global exception handler.
I have also face the same issue in Laravel version 7.3 where the AuthorizationException is not caught. What I come to know that we have to include AuthorizationException in the Handler.php like
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\Access\AuthorizationException;
use Throwable;
use Exception;
use Request;
use Response;
class Handler extends ExceptionHandler
{
// ...
/**
* 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 AuthorizationException)
{
return response()->json(['message' => 'Forbidden'], 403);
}
if ($exception instanceof ModelNotFoundException && $request->wantsJson()) {
return response()->json(['message' => 'resource not found')], 404);
}
return parent::render($request, $exception);
}
// ...
}
FYI if you just add the AuthorizationException by using the following statement
use AuthorizationException;
It still not working. So we have to specify the fully qualified namespace path.
i have this blogsController, the create function is as follows.
public function create() {
if($this->reqLogin()) return $this->reqLogin();
return View::make('blogs.create');
}
In BaseController, i have this function which checks if user is logged in.
public function reqLogin(){
if(!Auth::check()){
Session::flash('message', 'You need to login');
return Redirect::to("login");
}
}
This code is working fine , but it is not what is need i want my create function as follows.
public function create() {
$this->reqLogin();
return View::make('blogs.create');
}
Can i do so?
Apart from that, can i set authantication rules , like we do in Yii framework, at the top of controller.
Beside organizing your code to fit better Laravel's architecture, there's a little trick you can use when returning a response is not possible and a redirect is absolutely needed.
The trick is to call \App::abort() and pass the approriate code and headers. This will work in most of the circumstances (excluding, notably, blade views and __toString() methods.
Here's a simple function that will work everywhere, no matter what, while still keeping your shutdown logic intact.
/**
* Redirect the user no matter what. No need to use a return
* statement. Also avoids the trap put in place by the Blade Compiler.
*
* #param string $url
* #param int $code http code for the redirect (should be 302 or 301)
*/
function redirect_now($url, $code = 302)
{
try {
\App::abort($code, '', ['Location' => $url]);
} catch (\Exception $exception) {
// the blade compiler catches exceptions and rethrows them
// as ErrorExceptions :(
//
// also the __toString() magic method cannot throw exceptions
// in that case also we need to manually call the exception
// handler
$previousErrorHandler = set_exception_handler(function () {
});
restore_error_handler();
call_user_func($previousErrorHandler, $exception);
die;
}
}
Usage in PHP:
redirect_now('/');
Usage in Blade:
{{ redirect_now('/') }}
You should put the check into a filter, then only let the user get to the controller if they are logged in in the first place.
Filter
Route::filter('auth', function($route, $request, $response)
{
if(!Auth::check()) {
Session::flash('message', 'You need to login');
return Redirect::to("login");
}
});
Route
Route::get('blogs/create', array('before' => 'auth', 'uses' => 'BlogsController#create'));
Controller
public function create() {
return View::make('blogs.create');
}
We can do like this,
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect('/to/another/route/')->with('status', 'An error occurred.'));
It's not a best practice to use this method, but to solve your question, you can use this gist.
Create a helper function like:
if(!function_exists('abortTo')) {
function abortTo($to = '/') {
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect($to));
}
}
then use it in your code:
public function reqLogin(){
if(!Auth::check()){
abortTo(route('login'));
}
}
public function create() {
$this->reqLogin();
return View::make('blogs.create');
}