Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

When a middleware instantiation leads to a Interop\Container\Exception\ContainerException, the exception is silenced, and execution continues #416

Closed
Ocramius opened this issue Dec 30, 2016 · 10 comments
Assignees

Comments

@Ocramius
Copy link
Member

Ocramius commented Dec 30, 2016

Test case will come, but defining a middleware pipeline config with invalid string services as middleware, or strings pointing to factories that throw exceptions, expressive simply ignores those exceptions and keeps going.

Test case will come, but basically, I'd expect:

explosion-animation

Expressive did:

this-is-fine

Also: sorry for the half-submitted initial issue: switching from windows to mac to windows to mac is messing with my ability to use a keyboard.

@Ocramius Ocramius changed the title When a middleware instantiation leads to a `Container When a middleware instantiation leads to a `Interop\Container\Exception Dec 30, 2016
@Ocramius Ocramius changed the title When a middleware instantiation leads to a `Interop\Container\Exception When a middleware instantiation leads to a Interop\Container\Exception\ContainerException, the exception is silenced, and execution continues Dec 30, 2016
@Ocramius Ocramius self-assigned this Dec 30, 2016
@Ocramius
Copy link
Member Author

Ocramius commented Dec 30, 2016

Turns out that this is a bug caused by the stratigility Dispatch.

Specifically, stratigility will simply catch any Throwable and then call the next middleware, skipping on the current one.

This makes sense if $next is a middleware designed for error handling, but not otherwise.

I'm unsure if this is expressive configuring the middleware pipeline incorrectly, or if stratiglity is wrong upfront.

Here's an expressive application showing the issue in practice:

<?php

use Zend\Expressive\Application;
use Zend\Expressive\Container\ApplicationFactory;
use Zend\ServiceManager\ServiceManager;

require_once __DIR__ . '/../vendor/autoload.php';

$config = [
    'debug' => false,
    'dependencies' => [
        'factories' => [
            Application::class => ApplicationFactory::class,
            'failing-middleware' => function () {
                throw new \Exception('This will fail at instantiation');
            },
            'index' => function () {
                die('this middleware should NEVER be reached');
            },
        ],
    ],
    'middleware_pipeline' => [
        'foo' => [
            'middleware' => [
                'failing-middleware',
            ],
            'priority' => 100,
        ],
        'routing' => [
            'middleware' => [
                ApplicationFactory::ROUTING_MIDDLEWARE,
                ApplicationFactory::DISPATCH_MIDDLEWARE,
            ],
            'priority' => 1,
        ],
        'error' => [
            'middleware' => [
            ],
            'error'    => true,
            'priority' => -10000,
        ],
    ],
    'routes' => [
        [
            'name' => 'home',
            'path' => '/',
            'middleware' => 'index',
            'allowed_methods' => ['GET'],
        ],
    ],
];

$config['dependencies']['services']['config'] = $config;

(new ServiceManager($config['dependencies']))->get(Application::class)->run();

Note: adding an error middleware doesn't seem to make any difference

@Ocramius
Copy link
Member Author

Ocramius commented Jan 2, 2017

As discussed with @xtreamwayz, these are the versions involved in this bug:

  • zendframework/zend-expressive:1.0.5
  • zendframework/zend-stratigility:1.3.1

Some wild guesses:

12:04:28 xtreamwayz | ocramius: I'm guessing you are using stratigility 1.3 and expressive 1.1 is not out. I don't think there is an option for raiseThrowables yet in 1.0.5.
12:04:48 xtreamwayz | Probably if you update composer and restrict to straigility 1.2 might fix it.

@moderndeveloperllc
Copy link
Contributor

moderndeveloperllc commented Jan 2, 2017

Going to Stratigility 1.3 can cause issues if you are using a config-driven setup vs. a procedural approach. Config-driven raise_throwables was implemented in develop with this commit, but that's 1.1 of course. Not sure if @weierophinney is going to want to backport this, or just set the 1.0.x branch to Stratigility 1.2. FWIW, it should also work fine if you add the line to your index.php:

/** @var Application $engine */
$engine = $container->get(Application::class);
$engine->raiseThrowables();

@Ocramius
Copy link
Member Author

Ocramius commented Jan 2, 2017

Indeed, hijacking raiseThrowables to set it as the default fixes the issue for me, but will not trigger the error middleware.

In my opinion, the entire idea of the error middleware being piped with all the others is kinda useless: probably to be deprecated in favor of just a $errorMiddleware($request, $response, $next) with the exception to be injected as a request attribute.

@weierophinney
Copy link
Member

In my opinion, the entire idea of the error middleware being piped with all the others is kinda useless: probably to be deprecated in favor of just a $errorMiddleware($request, $response, $next) with the exception to be injected as a request attribute.

This is the direction we're moving for 1.1, actually, though without using request attributes.

What will happen instead is that the raise_throwables flag will be enabled by default with new applications, and users will use middleware for handling errors.

Error conditions will typically just be exceptions:

function ($request, $response, $next)
{
    throw new RuntimeException('Oh, no, I cannot handle this!');
}

You'll then have middleware in an outer layer that handles exceptions. We will ship a default implementation, but you can also use your own, or stack multiple such middleware each capable of handling specific exceptions:

function ($request, $response, $next)
{
    try {
        $response = $next($request, $response);
        return $response;
    } catch (UnauthorizedException $e) {
    } catch (Throwable $e) {
        throw $e;
    }
    
    // Handle UnauthorizedException; maybe display a login form?
    // ...
    return $someNewResponse;
}

Stratigility's "error middleware" concept from 1.0 is deprecated starting in 1.3, and will be removed entirely in 2.0 in favor of this concept.

Regarding your specific error, I was able to reproduce it. For those that wonder, the results of executing the above application are:

this middleware should NEVER be reached

What SHOULD happen is that the failing-middleware callback will raise an exception, and the Stratigility Next/Dispatch implementation will call the first error middleware in the stack (which is empty currently). My suspicion is that this is a subtle bug in Stratigility, and I'll see if I can find a solution for it today.

Thanks for the detailed report, @Ocramius!

@weierophinney
Copy link
Member

Found the issue, and it's in Stratigility.

The crux of the issue is that internally, Dispatch checks to see if the middleware provided implements the http-middleware interfaces; if so, it dispatches it differently. That "differently" means that the $err argument is ignored from that point forward.

Expressive's ErrorMiddlewarePipe does not implement http-middleware interfaces, but it composes a MiddlewarePipe... and in Stratigility 1.3, that class does implement these interfaces. That means that as the middleware the ErrorMiddlewarePipe composes is dispatched, the error is silently dropped.

The solution will likely be to test in Dispatch for the following:

if ($route->handler instanceof ServerMiddlewareInterface
    && ! $route->handler instanceof MiddlewarePipe
) {
    return $this->dispatchInteropMiddleware(/* ... */);
}

I'll open an issue on Stratigility, and, once fixed, we'll do another 1.0.X release of Expressive that requires that version or above (or versions prior to the 1.3 series).

@weierophinney
Copy link
Member

Confirmed: the change I suggested does work. I'll get a test case written for Stratigility, and submit a patch there.

weierophinney added a commit to weierophinney/zend-stratigility that referenced this issue Jan 5, 2017
…esent

As reported in zendframework/zend-expressive#416, error middleware
nested inside a `MiddlewarePipe` was not being dispatched. This was due
to the fact that `Dispatch` was identifying the pipeline as http-interop
middleware, and thus dropping the `$err` argument (as interop middleware
cannot accept that argument).

Theis patch updates `Dispatch` to check if the middleware is a
`MiddlewarePipe` and a non-null `$err` is present; if so, it now
dispatches it as callable middleware instead of as interop middleware.
@Ocramius
Copy link
Member Author

Ocramius commented Jan 5, 2017

@weierophinney will this be fixed by zendframework/zend-stratigility#95? Specifically: does the example application above crash correctly when run against that patch? (sorry, not on a dev environment atm)

@weierophinney
Copy link
Member

@Ocramius yes, application crashes correctly when run against that patch; I've tested against your example both with and without the patch, and the patch makes it behave correctly.

@Ocramius
Copy link
Member Author

Ocramius commented Jan 5, 2017

@Ocramius Ocramius closed this as completed Jan 5, 2017
@Ocramius Ocramius self-assigned this Jan 5, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants