11

I have a laravel controller that can throw an exception, and a global middleware that catches that exception. In semi pseudo code:

// App\Controllers\...
class Controller {
  function store() {
    throw new FormException; // via validation etc, but it's thrown here
  }
}

// App\Http\Middleware\...
class Middleware {
  function handle(Closure $next) {
    try {
      // Breakpoint 1
      return $next(); // $response
      // Breakpoint 2
    }
    catch (FormException $ex) {
      // Breakpoint 3
      exit('FormException caught!');
    }
  }
}

The problem is that the exception is never caught. Somwhere in the pipeline, the application catches the exception and prints a pretty error page, but it should be caught by my middleware so it can handle it properly.

  • Breakpoint 1 should trigger, and it does << good
  • Breakpoint 2 shouldn't trigger, and it doesn't << good
  • Breakpoint 3 should trigger, but it doesn't << what??

The only way I can imagine my middleware not catching it, is if it's caught somewhere deeper inside the pipeline, not further up/around, but I can't find any try/catch in other middleware, or in the pipeline execution code.

Where is this exception caught? Why?

This might not be a great pattern, but I don't care about that now. I'm more curious than anything else. Do I completely misunderstand Laravel's middleware?

My own super simple middleware test does what I expected: https://3v4l.org/Udr84 - catch and handle exception inside middleware.

Notes:

  • The $response object (return value of $next()) is the handled exception page, so it has already been handled. Where and why?
  • Handling the exception in App\Exceptions\Handler::render() works, but I want all logic to be in a middleware package, not in app code.

Relevant Laravel code:

  • Kernel::handle() starts the middleware pipeline << this has a catch-all catch(), but my catch() comes first, right?
  • Pipeline::then() starts the middleware execution
  • Pipeline::getSlice() handles and creates the $next closures
Rudie
  • 52,220
  • 42
  • 131
  • 173

5 Answers5

20

Apparently this is by design:

Yes, this is the beavhiour starting from L5.2. Throwing an exception causes the response to be set as that returned from the exception handler, and then the middleware is allowed to backout from that point.

I think that's very strange. Pluggable middleware would be perfect for catching exceptions.

Two ways to still do this:

Rudie
  • 52,220
  • 42
  • 131
  • 173
  • i agree: exception handling in middleware would be usefull in packages: i'm developing a middleware to handle database transactions and i need to commit / rollback from the middleware. Being able to catch exceptions would have been very useful. Now i have to go the 'funky' way ;) – Moppo Oct 15 '16 at 09:59
  • Thanks a lot Rudie. '**Funky way**' saved my day :) Working properly on Laravel **v5.5.33** – Vinay Vissh Feb 04 '18 at 15:03
6

I had the same problem. When reading the thread Rudie mentioned, they give a possible solution there which worked for me:

public function handle(Request $request, Closure $next) {
  $response = $next($request);

  // 'Catch' our FormValidationException and redirect back.
  if (!empty($response->exception) && $response->exception instanceof FormValidationException) {
    return redirect()->back()->withErrors($response->exception->form->getErrors())->withInput();
  }

  return $response;
}
Jetse
  • 1,706
  • 2
  • 16
  • 22
  • That's what I got on Github too. That's just way too nasty for me. Exceptions are to be caught, right? Not extracted from a Response object.. Depending on your use case, you could edit the App's exception handler renderer. – Rudie Sep 28 '16 at 14:55
  • @Rudie I totally agree, it is unexpected behaviour. But I use the default exception handler for my base application. For my api route group I want to handle errors different, then this method works. – Jetse Sep 29 '16 at 09:16
3

Looking at the source code, you need to catch both \Exception and \Throwable for your try catch to properly work in your middleware. This works on Laravel 5.8

class TryCatchMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */


    public function handle($request, Closure $next)
    {

        try {
           if ( somethingThatCouldThrowAnException() ) {
                $request->newVariable = true;
           }
        } catch (\Exception $e) {
            // do nothing
        } catch (\Throwable $e) {
            // do nothing
        }

        return $next($request);
    }
}
Ray
  • 177
  • 2
  • 8
  • I don't want to catch from `somethingThatCouldThrowAnException()`, but from `$next`. Unfortunately that's part of the middleware stack with its own exception catching. – Rudie Jan 05 '20 at 15:52
0

How catch errors without touching App\Exceptions\Handler file:

Register your CustomExceptionHandler

/* @var ExceptionHandler Illuminate\Contracts\Debug\ExceptionHandler */
$previousHandler = null;
if (app()->bound(ExceptionHandler::class) === true) {
    $previousHandler = app()->make(ExceptionHandler::class);
}
app()->singleton(ExceptionHandler::class, function () use ($previousHandler) {
    return new CustomExceptionHandler($previousHandler);
});

And your basic CustomExceptionHandler

class CustomExceptionHandler implements ExceptionHandlerInterface
{
    /**
     * @var ExceptionHandlerInterface|null
     */
    private $previous;

    public function __construct(ExceptionHandlerInterface $previous = null)
    {
        $this->previous = $previous;
    }

    public function report(Exception $exception)
    {
        $this->previous === null ?: $this->previous->report($exception);
    }

    public function render($request, Exception $exception)
    {
        if ($exception instanceof CustomExceptionHandler) {
            echo 'This is my particular way to show my errors';
        } else {
            $response = $this->previous === null ? null : $this->previous->render($request, $exception);
        }

        return $response;
    }

    /**
     * {@inheritdoc}
     */
    public function renderForConsole($output, Exception $exception)
    {
        /* @var OutputInterface $output */
        $this->previous === null ?: $this->previous->renderForConsole($output, $exception);
    }
}
pablorsk
  • 3,861
  • 1
  • 32
  • 37
  • Only 1 package can do this. What if 2 packages want to add exception handling for their own exception? The funky `$response->exception` method at least can do that. Each has its advantage. – Rudie Feb 04 '18 at 16:42
-3

I think I can see why your code doesn't catch exceptions. Please try using the following code for your handle method:

function handle(Closure $next) {
try {
  // Breakpoint 1
  $response = $next();
  // Breakpoint 2
}
catch (FormException $ex) {
  // Breakpoint 3
  exit('FormException caught!');
}
return $response;
}

The code above has not been tested but as you look at the Laravel documentation you can see before returning the response, you should perform your code (in this case, your exception handling logic). Please look at: Laravel - Defining Middleware for more information on Before & After Middleware definition.

And by the way, also look at this file: Laravel/app/Exceptions/Handler.php which I believe is a better place to handle your exceptions globally.

  • That's not it. There's no exception to be caught. It's caught somewhere else. The `$response` object in my handler is the page that displays the handled exception. Where and why is it handled? – Rudie Jul 31 '16 at 11:00
  • Handling it in the app's handler works, but I want a middleware package to do this. This is why middleware exists, isn't it? To do something before and after the action. – Rudie Jul 31 '16 at 11:04