zendframework / zend-stratigility

Middleware for PHP built on top of PSR-7 and PSR-15

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Error handling

mindplay-dk opened this issue · comments

Me and a colleague have been working hard trying to bypass the error handler - we want to use an alternative error handler (booboo) but the middleware stack catches and handles exceptions before they can get to the error handler.

We noticed the onerror callback in $options of the FinalHandler constructor, but this gets constructed internally in MiddlewarePipe, where everything is declared private, so we had no luck attempting to extend the class in order to provide the option.

How do we turn off built-in error handling?

I made a simple draft implementing booboo in stratigility, and i can't find a problem with it. Did you tried to implement Zend\Stratigility\ErrorMiddlewareInterface?

@HardieBoeve I didn't even notice that interface existed. Care to paste your draft here, or in a gist maybe? :-)

Is there a manual for this library? The README is very thin and doesn't link to any documentation - all I've had to go on is docblocks and poking through the code, I completely missed that interface...

@mindplay-dk i made a small draft that implements the Zend\Stratigility\ErrorMiddlewareInterface, you can find it here.

The docs for the interface and more are in the doc/book directory in stratigility.

Although it's not possible to set booboo or another error handler as the default can't be overridden (yet).
But in my opinion i don't see why this is necessary, because stratigility provides 2 ways to handle errors.

It's necessary because we want to handle all errors the same way, and errors are handled differently in development, staging and production scenarios. Error handling shouldn't even happen in the middleware layer, at all, because it's not the only layer in which errors can occur... I need those errors to bubble to a global handler, the same as any other error. I will fork and submit a PR if I have to.

We already support two ways of doing this.

The first is to use error handler middleware. This is actually likely the appropriate way to handle it, as you can stack them, and have them fall-through. So, for instance, the first one might be for development, the next for staging, the last for production; based on environment, they may simply continue calling $next() until the last handles the error:

$env = $_SERVER['THIS_TELLS_ME_THE_ENV'];

$app->pipe(function ($err, $req, $res, $next) use ($env) {
    if ($env !== 'development') {
        return $next($req, $res, $err);
    }
    // handle it, and perhaps even have it fall-through by returning $next()
});

$app->pipe(function ($err, $req, $res, $next) use ($env) {
    if ($env !== 'staging') {
        return $next($req, $res, $err);
    }
    // handle it, and perhaps even have it fall-through by returning $next()
});

// etc.

This works already. Alternatively, it could be a single error handler:

$env = $_SERVER['THIS_TELLS_ME_THE_ENV'];

$app->pipe(function ($err, $req, $res, $next) use ($env) {
    switch ($env) {
        case 'development':
        case 'staging':
        case 'production':
        default:
    }
});

Either way, the thing to remember is that the "final handler" is simply what is executed if no middleware returns a response, _including* error middleware. In other words, if you write good error middleware, you can handle *anything_.

The second way to do it is to pass your handler when invoking the middleware. MiddlewarePipe::__invoke() takes a request, a response, and optionally a callable $out argument:

$finalResponse = $app($request, $response, function ($req, $res, $err = null) {
    // do something in the event that no middleware returned a response, or no
    // error middleware handled a raised error
});

If you are using Zend\Diactoros\Server, you can manage this using the following:

$server->listen(function ($req, $res) use ($app, $errorHandler) {
    return $app($req, $res, $errorHandler);
});

Again, $out, as a "final handler", is triggered only if no response is returned and the queue is depleted. If you have good error middleware, it will never be invoked.

So, my recommendation is: use error middleware. That's why the feature exists, and your final handler really needs to be a mechanism of last resort. If you have a default error handler stacked early in the queue, you can handle any error that occurs just as easily, and in a way that's more easily replaced, than with the final handler.

So, my recommendation is: use error middleware. That's why the feature exists, and your final handler really needs to be a mechanism of last resort.

I get that, but on development systems, we actually have an error-handler: xdebug. We don't want custom error handling on developer's machines. So what's missing, still, is some way to disable error handling entirely (?)

Come to think of it, on production systems, we also don't want dedicated error-handling in the middleware-stack - we want a PHP error-handler that is going to trap errors everywhere, because we use the same core architecture for web requests as we do for e.g. command-line scripts. (we have services outside the scope of web-requests, e.g. scenarios where something could fail before the middleware-stack even loads.)

So we really need to not have any specialized error-handling in the middleware-stack, at all.

@mindplay-dk I think you're not understanding the role of error middleware. It is not invoked automatically based on exceptions or PHP errors. It is invoked by your own middleware, so that your application can handle an error condition.

As an example:

$app->pipe(function ($req, $res, $next) {
    try {
        doSomethingThatCouldRaiseAnException();
        $next($req, $res);
    } catch (Exception $e) {
        $next($req, $res, $e); <-- THIS INVOKES ERROR MIDDLEWARE
    }
});
$app->pipe(function ($err, $req, $res, $next) {
    // This will receive the exception passed to $next() from above
});

The FinalHandler only exists for those situations where the middleware stack is exhausted without returning a response. Examples of this include:

  • No middleware was executed: e.g., all middleware was piped using a route, but the URI did not match any of those routes; all middleware called $next(), but none returned a response.
  • No error middleware handled an error: e.g., an executed middleware called $next with an error, but no error middleware was registered, or they all passed on to $next.

If such a condition happens, we need to do something in order to fulfill our contract and return a response. That is the only reason the FinalHandler exists. And the FinalHandler is essentially a type of error handler. The main difference is that there may or may not be an error in place; if there's not the "error" is that either no middleware was invoked, or none of them returned a response.

So, with this in mind, and considering your last two comments, what you likely want are a combination of the following:

  • no registered error middleware in your application. We don't register any by default, so you'll be fine in that regard.
  • a custom final handler (the "$done" or "$out" callable to pass to the initial middleware) that just returns the response passed to it. I showed you how to register such a handler in my previous comment.

Thanks for the detailed information - I'm still blurry on a couple of details, mainly, if the error handler is not invoked automatically based on exceptions or PHP errors, then how come exceptions and errors aren't handled normally (e.g. by xdebug) when an error/exception occurs during dispatch? (All I see is a plain text error-message, not the colorful detailed xdebug stack trace I normally see.)

Integrating league/booboo is next on my to-do list tomorrow morning, so maybe it'll make more sense when I get deeper into it. Thanks again :-)

then how come exceptions and errors aren't handled normally (e.g. by xdebug) when an error/exception occurs during dispatch?

Interestingly, when testing my own website (which uses Stratigility), I do see the nice XDebug colorized HTML output on errors, which tells me it may be a configuration issue. ;-)

Good luck integrating booboo; let me know how it goes!

when testing my own website (which uses Stratigility), I do see the nice XDebug colorized HTML output on errors

You were right of course - I guess from the very simple error message that gets rendered when you hit a missing URL, I got the impression this was standard error handling for exceptions; I assumed you were throwing an exception if the stack was exhausted and no middleware kicked in.

That's what I would have done personally - leave the actual error-handling to whoever is dispatching the middleware stack; rather than a printed error message, throw an exception and expect a developer to decide whether to print the error message, dump the stack, pass to an error handler, log it, etc.

Quoting you:

The FinalHandler only exists for those situations where the middleware stack is exhausted without returning a response

The common behavior for components that have exhausted all of their options, is to throw an exception - I would argue that, trying to handle missing/bad configuration by introducing arbitrary middleware isn't really a clean solution, and doesn't prompt me as a developer to take action; an exception with a developer-friendly error message and a stack trace does that much better.

Just my two cents :-)

Ah, here's my problem - you don't have any actual error-handling, that much is true, but you do have a try/catch that traps every exception, and since I relay errors to ErrorException throws, well, there you go, the middleware stack is effective working like a global error handler.

In other words, booboo is never actually triggered, because strictly speaking, there is no exception - diactoros has handled it. Except of course, it can't actually handle all exceptions in any meaningful way, no exception handler can, which is why it's generally frowned upon to catch all exceptions; it's really only acceptable to do that in the equivalent top-level script in your php application. It's generally considered best practice, if you cannot handle an exception completely, then you should let it bubble up to someone who can.

So I had to install error middleware that takes the exception that was trapped by the middleware stack, and manually passes it to booboo for processing, and that "works", but only partially fixes the problem, because what I actually want, in a development environment, is no error handling, e.g. let xdebug handle the error. Now, instead, I have to settle for booboo's inferior table formatter, which is much less helpful compared to xdebug, and doesn't have xdebug's configuration options - different developers want different settings.

Of course, I can extend and override things and replace the Dispatch component inside the middleware stack, but jeez... it was already a lot of work to figure this out - debugging when something is catching all exceptions is really difficult. But now I have to extend and override several components to get around this... it's going to be far from simple :-/

@mindplay-dk

Thanks for the sleuthing; I kind of remembered the try/catch block, but was not connecting it to the error handling arguments you were making.

Regarding those:

it's generally frowned upon to catch all exceptions; it's really only acceptable to do that in the equivalent top-level script in your php application

For it being "generally frowned upon," I have yet to use a framework that does not do it. Frameworks tend to do this in order to ensure that something is delivered to the end-user when an error condition occurs, instead of either a blank screen (best case scenario) or an error/exception trace (worst-case, when in production!). Symfony's HttpKernel does it, ZF2's DispatchListener does it, Slim provides error handler middleware, etc. In each case, they provide a default implementation to ensure something is presented back to the requesting client, but simultaneously allow end-users to customize the behavior (via event listeners in Symfony and ZF2, error middleware in Slim, etc.).


When I suggested you use error middleware, I also suggested you branch the logic based on environment; that might be via environment variables, a value you set in the request, or something else, but the idea is:

if ($isDevelopment) {
    if ($err instanceof \Exception) {
        throw $err;
    }
    if (is_string($err)) {
        throw new Exception($err);
    }
    // handle other types of $err?
} else {
    // pass to booboo
}

In the above example, during development, exceptions are re-thrown, which means that they would need to be handled by your exception handler. This would give you the ability to pass control back to XDebug. In production, you would use booboo, and have the additional decision of determining what, if anything, you return back to the client.

Will I consider an alternate dispatcher or a flag in the dispatcher for disabling the try/catch block? Maybe. But for the majority of users, having to register and/or create an error and/or exception handler out of the box for the most common use cases (404 not found, 500 server error) makes the initial experience more difficult. Since you can accomplish it via error middleware, I'd need convincing that another approach is necessarily better and easier for end users.


Regarding this statement you made about Next delegating to the final handler after exhausting its stack:

The common behavior for components that have exhausted all of their options, is to throw an exception

Except that inability to match a URI is expected within Stratigility. Users enter incorrect URIs all the time. Exhausting the stack without an error occurring is expected, and in such events, we want to return a 404. If you want to provide a custom 404 page, you either register middleware to run last to display the 404, or you provide a custom FinalHandler that will do it. Asking users to provide their own 404 middleware out-of-the-box is a terrible user experience; asking them to do it if they want to provide a nicer 404 page is reasonable.

I have yet to use a framework that does not do it.

Absolutely. But is diactoros a framework? In my setup, it's just one
component in a much larger framework that includes error handling
(obviously) and session management, logging, an event bus, and a ton of
other things that, combined, make up my framework.

I didn't see diactoros as a framework, I saw as just one component that
does one thing: abstracting the request/response cycle. That's all I need
it to do.

You have concerns about "the majority of users", which makes it sound like
your main concern is making things easy. If so, you have a long way to go,
and a ton of features still missing, at the very least cookies and session
management. It takes a developer, not a user, to build a full stack with
those missing features - a user simply can't do that.

It's your product, so I can't tell you what to do, but, if I were you, I
would focus on keeping things simple - as opposed to making things easy.
Lose the training wheels - they may help non-developers getting started,
but they only slow down developers and add complexity.

Sometimes less is more. I don't want a framework, I want a component that
does one thing well. IMO components with a strong focus (a smaller scope)
are much easier to piece together and build a framework, which is what I'm
doing.

But maybe that's my misunderstanding and the reason I have already spent a
lot more time than I was expecting, just setting this up. I never saw this
library as my framework, and it wouldn't work as a framework for me,
because the framework I'm building doesn't even always load the middleware
stack, e.g. when running under the command line. And therefore, for one,
can't be my global exception handler, and only gets in the way when it
tries to be.

Handling the 404 case arguably might be in scope, and error handling
middleware probably makes sense for that. But systemically handling errors
in my code, or in the code of other third-party components I might use in
my code? I don't expect, need, or want that, of any component.

I respect that you may see things differently, I'm just giving you my point
of view :-)

But so far, for someone attempting to build a full framework from third
party components, this one has caused by far the most problems, and, in my
opinion, mostly because it oversteps its responsibilities. I respect the
fact that it may just be overstepping the responsibility I need it to
have ;-)
On Jul 30, 2015 22:47, "weierophinney" notifications@github.com wrote:

@mindplay-dk https://github.com/mindplay-dk

Thanks for the sleuthing; I kind of remembered the try/catch block, but
was not connecting it to the error handling arguments you were making.

Regarding those:

it's generally frowned upon to catch all exceptions; it's really only
acceptable to do that in the equivalent top-level script in your php
application

For it being "generally frowned upon," I have yet to use a framework that
does not do it. Frameworks tend to do this in order to ensure that
something is delivered to the end-user when an error condition occurs,
instead of either a blank screen (best case scenario) or an error/exception
trace (worst-case, when in production!). Symfony's HttpKernel does it,
ZF2's DispatchListener does it, Slim provides error handler middleware,
etc. In each case, they provide a default implementation to ensure
something is presented back to the requesting client, but
simultaneously allow end-users to customize the behavior (via event

listeners in Symfony and ZF2, error middleware in Slim, etc.).

When I suggested you use error middleware, I also suggested you branch the
logic based on environment; that might be via environment variables, a
value you set in the request, or something else, but the idea is:

if ($isDevelopment) { if ($err instanceof \Exception) { throw $err; } if (is_string($err)) { throw new Exception($err); } // handle other types of $err?} else { // pass to booboo}

In the above example, during development, exceptions are re-thrown, which
means that they would need to be handled by your exception handler. This
would give you the ability to pass control back to XDebug. In production,
you would use booboo, and have the additional decision of determining what,
if anything, you return back to the client.

Will I consider an alternate dispatcher or a flag in the dispatcher for
disabling the try/catch block? Maybe. But for the majority of users, having
to register and/or create an error and/or exception handler out of the box
for the most common use cases (404 not found, 500 server error) makes the
initial experience more difficult. Since you can accomplish it via error
middleware, I'd need convincing that another approach is necessarily

better and easier for end users.

Regarding this statement you made about Next delegating to the final
handler after exhausting its stack:

The common behavior for components that have exhausted all of their
options, is to throw an exception

Except that inability to match a URI is expected within Stratigility.
Users enter incorrect URIs all the time. Exhausting the stack without
an error occurring is expected, and in such events, we want to return a
404. If you want to provide a custom 404 page, you either register
middleware to run last to display the 404, or you provide a custom
FinalHandler that will do it. Asking users to provide their own 404
middleware out-of-the-box is a terrible user experience; asking them to do
it if they want to provide a nicer 404 page is reasonable.


Reply to this email directly or view it on GitHub
#16 (comment)
.

But is diactoros a framework?

Does it matter? From your arguments, it clearly matters to you. The question is, does it matter to the intended audience?

I've done a bit of brainstorming around this, particularly as my team has been working on zend-expressive, which is intended to be to Stratigility what ExpressJS is to Connect: in other words, Stratigility is a middleware foundation and library, while Expressive is a microframework built on top of it to provide ease-of-use.

In that regard, we're now looking at the error handling in a similar vein to what you outline above; as a low-level component, it may be but one aspect of our application, and we want it to "play nice" with the rest of the application workflow.

On the flip side, you can use Stratigility currently without adding any error handling because of the way the Dispatcher is implemented. Considering you don't want your errors leaking in a web application, this is a reasonable solution for first-comers.

Also, as you note, 404 is definitely in scope for a final handler; however, that can be done now without doing any other error handling; 404 is the condition when the stack is exhausted, and the response passed to the final handler differs from the response at application invocation. In other words, it has nothing to do with error handling, and would still "just work" even with removal of the try/catch block in the Dispatcher.

What all this means is: we have two audiences:

  • Those who are going to consume and use Stratigility in small projects, who want something lean and easy to pick up with very few extra dependencies, and who may or may not be relatively new to programming.
  • Those who are planning to write their own application architecture and consume Stratigility as part of that architecture.

For the first camp, removing the try/catch block may or may not lead to more WTF moments as they start having to deal with exceptions raised.

For the second camp, the try/catch block can be an impediment to their architecture.

My feeling is: let's remove that try/catch block. However, doing so is a BC break at this time. Currently exceptions are caught and then passed to any existing error handlers, and removal of the try/catch breaks that workflow. I suggest the following:

  • Create an alternate Dispatcher that removes the try/catch. This dispatcher will be opt-in when first introduced.
  • To opt-in to the new Dispatcher, two new changes will be necessary:
    • Next's constructor will need an additional optional argument, the Dispatcher instance.
    • MiddlewarePipe will need a setter for injecting a Dispatcher instance (which it will then pass to Next if it lazy-loads an instance during invocation). A protected (protected, so it can be overridden in extending classes) getter would be used inside of __invoke() to retrieve it and pass it to the constructor of Next.
    • The above could potentially be only class names or callables as well.
  • Document how to inject an alternate Dispatcher.

The above would be for an upcoming minor version, and we would message that applications should implement their own try/catch blocks if they want to pass exceptions on to error middleware reliably going forward.

This would also allow consumers such as yourself to extend MiddlewarePipe and set your own default dispatcher, which would work reliably going forward.

For the next major version, we would swap the default dispatcher used.

@mindplay-dk Sound reasonable? Are you interested in submitting a PR with this?

This is implemented as an opt-in feature in the 1.3 series (if you call raiseThrowables()), and as the only method for error handling in the upcoming 2.0 (current develop branch).