Strange behaviour of ImplicitOptionsMiddleware with multiple route on same path

I’m trying to retrieve the allowed methods for a specific endpoint.
The problem is when I try to do a OPTIONS request the ImplicitOptionsMiddleware match only the last route configured and retrieves the corrisponding allowed method.

Code to reproduce the issue

This is my route configuration based on the example here

public function getRoutesConfig(): array {
		return [
			[
				'name' => 'api.v1.users.me.get',
				'path' => '/api/v1/users/me',
				'middleware' => [
					Action\GetUserMeAction::class
				],
				'allowed_methods' => ['GET']
			],
			[
				'name' => 'api.v1.users.me.put',
				'path' => '/api/v1/users/me',
				'middleware' => [
					Action\UpdateUserMeAction::class
				],
				'allowed_methods' => ['PUT']
			]
		];
	}

I don’t want to configure all in one because I would need to create a middleware which will dispatch to the correct action.
I don’t understand whether this is a bug or a implementation decision.

Expected results

This is the result that I want to achieve where the allowed methods are merged.

Access-Control-Allow-Headers →Authorization, Content-Type
Access-Control-Allow-Origin →*
Allow →GET, PUT
Connection →Keep-Alive
Content-Length →0
Content-Type →text/html; charset=UTF-8
Date →Wed, 28 Feb 2018 10:31:55 GMT
Keep-Alive →timeout=5, max=100
Server →Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.1.14
X-Powered-By →PHP/7.1.14

Actual results

Access-Control-Allow-Headers →Authorization, Content-Type
Access-Control-Allow-Origin →*
Allow →PUT
Connection →Keep-Alive
Content-Length →0
Content-Type →text/html; charset=UTF-8
Date →Wed, 28 Feb 2018 10:31:55 GMT
Keep-Alive →timeout=5, max=100
Server →Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.1.14
X-Powered-By →PHP/7.1.14

Which router are you using?

I ask, because the way ImplicitOptionsMiddleware works is to grab the allowed methods for the route from the RouteResult. Each router implementation has to determine for itself how that will work. In the case of zend-router, it does this by attaching child Method routes for the more specific HTTP method matches; when a route match fails due to the method, it can look at those child routes to determine what to do. FastRoute is able to differentiate between failures due to matching versus method, and gives us the information straight-away; AuraRouter does similarly.

Essentially, I need to track down which router is not getting you a full list of allowed HTTP methods, so we can determine which adapter package to patch.

I’m using the zendframework/zend-expressive-zendrouter 2.1.0.

Hello,

I’ve just tested it with Expressive 3.0.0rc1 with all router adapters and it seems to not working correctly for all of them, here is my configuration in routes.php:

    $app->get('/api/v1/users/me', App\Handler\GetAction::class, 'get');
    $app->put('/api/v1/users/me', App\Handler\UpdateAction::class, 'update');

and always OPTIONS request to /api/v1/users/me give me only ONE allowed method in response header…

I’m investigating that issue now.

I’m analyzing the function marshalSuccessResultFromRouteMatch inside the ZendRouter class and from what I can understand the RouteResult will contain the allowedMethods of the route matched. This is a temporary patch until someone more expert than me can find the right solution:

/**
	 * Create a successful RouteResult from the given RouteMatch.
	 *
	 * @param RouteMatch $match
	 * @param PsrRequest $request Current HTTP request
	 * @return RouteResult
	 */
	private function marshalSuccessResultFromRouteMatch(RouteMatch $match, PsrRequest $request) {
		$params = $match->getParams();

		if (array_key_exists(self::METHOD_NOT_ALLOWED_ROUTE, $params)) {
			return RouteResult::fromRouteFailure(
							$this->allowedMethodsByPath[$params[self::METHOD_NOT_ALLOWED_ROUTE]]
			);
		}

		$routeName = $this->getMatchedRouteName($match->getMatchedRouteName());

		$route = array_reduce($this->routes, function ($matched, $route) use ($routeName) {
			if ($matched) {
				return $matched;
			}

			// We store the route name already, so we can match on that
			if ($routeName === $route->getName()) {
				return $route;
			}

			return false;
		}, false);

		if (!$route) {
			// This should never happen, as Zend\Expressive\Router\Route always
			// ensures a non-empty route name. Marking as failed route to be
			// consistent with other implementations.
			return RouteResult::fromRouteFailure();
		}
                 
                // FIX HERE
		if ($request->getMethod() === 'OPTIONS') {
			$route = new Route($route->getPath(), $route->getMiddleware(), $this->allowedMethodsByPath[$route->getPath()], $route->getName());
		}

		return RouteResult::fromRoute($route, $params);
	}

@dominicdettabp
I’ve prepared fix for v3:
https://github.com/zendframework/zend-expressive-router/pull/53 (and adapters libraires)

As I’ve checked the same issue we have in v2, and I’m gonna work on it later on. As I discussed with @mwop we are going to have these fixes in zend-expressive-router after moving these middlewares there (see: Roadmap: Expressive 2.2)

Just update: we have fixed the issue in v3 already - zend-expressive-router 3.0.0rc4 is with fixed Implicit*Middlewares. Now @matthew is working on backporting these fixes into v2.

Hi, I saw that the zend-expressive-router 2.4 has been merged but after a composer update I receive the following error:

<b>Deprecated</b>: Zend\Expressive\Router\Route will not accept anything other than objects implementing the MiddlewareInterface starting in version 3.0.0. Please update your code to create Zend\Expressive\Router\Route instances using MiddlewareInterface instances. in <b>/var/www/html/vendor/zendframework/zend-expressive-router/src/Route.php</b> on line <b>90</b><br />

The problem is that my middleware is implemeting the correct interface:

use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
class ListLanguagesAction implements ServerMiddlewareInterface {
....
}

What I need to do to solve the deprecation error?

What version of webimpress/http-middleware-compatibility is installed?

What version of http-interop/http-middleware is installed?

The changes in 2.4 should work with any combination of the two above, but I’d like to test the specific combination you’re using to see if I can reproduce.

Thanks!

Thanks for the fast reply.

webimpress/http-middleware-compatibility 0.1.4
http-interop/http-middleware 0.4.1

I will try to install a new project with the same dependencies.

It’s a problem with the 2.4.0 release. We’re specifically testing against the webimpress/http-middleware-compatibility MiddlewareInterface, when we should be checking against any of the various http-interop interfaces. I’ll get a bugfix out shortly.

I will try tomorrow. As always thanks for your awesome and fast support.

Interestingly, I cannot reproduce this, @dominicdettabp.

I tried two different things:

  • I wrote a unit test to see if we’d trigger the deprecation notice for any form of http-interop middleware. It passes.
  • I installed the v2 skeleton, which installs http-interop 0.4.1, the 0.1.4 release of the polyfill, and, more importantly, defines the default actions using the 0.4.1 ServerMiddlewareInterface. These just worked.

In investigating a bit more, the polyfill actually creates a class_alias of the polyfill MiddlewareInterface to whatever middleware interface is defined in the http-interop version installed. As such, instanceof checks against the polyfill interfaces are equivalent to those against the http-interop interface, which is why my new tests were not producing errors.

I’ve also run the tests against PHP 5.6 as well as PHP 7 versions; the behavior remains the same.

Run composer update in your project, to ensure all dependencies are up-to-date, and let me know if you continue to see the problem. If you do, I’ll likely have you try and isolate a minimal example that reproduces the issue.

Ok, I installed a new zend-expressive skeleton project with the following command:

composer create-project zendframework/zend-expressive-skeleton

These are my dependency with composer show:

container-interop/container-interop      1.2.0              Promoting the interoperability of container objects (DIC, SL, etc.)
doctrine/instantiator                    1.1.0              A small, lightweight utility to instantiate objects in PHP without invoking their constructors
fig/http-message-util                    1.1.2              Utility classes and constants for use with PSR-7 (psr/http-message)
http-interop/http-middleware             0.4.1              Common interface for HTTP server-side middleware
mtymek/blast-base-url                    0.2.0              PSR-7 middleware and helpers for working with base URL.
myclabs/deep-copy                        1.7.0              Create deep copies (clones) of your objects
phar-io/manifest                         1.0.1              Component for reading phar.io manifest information from a PHP Archive (PHAR)
phar-io/version                          1.0.1              Library for handling version information and constraints
phpdocumentor/reflection-common          1.0.1              Common reflection classes used by phpdocumentor to reflect the code structure
phpdocumentor/reflection-docblock        4.3.0              With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve in...
phpdocumentor/type-resolver              0.4.0             
phpspec/prophecy                         1.7.5              Highly opinionated mocking framework for PHP 5.3+
phpunit/php-code-coverage                5.3.0              Library that provides collection, processing, and rendering functionality for PHP code coverage information.
phpunit/php-file-iterator                1.4.5              FilterIterator implementation that filters files based on a list of suffixes.
phpunit/php-text-template                1.2.1              Simple template engine.
phpunit/php-timer                        1.0.9              Utility class for timing
phpunit/php-token-stream                 2.0.2              Wrapper around PHPs tokenizer extension
phpunit/phpunit                          6.5.7              The PHP Unit Testing framework.
phpunit/phpunit-mock-objects             5.0.6              Mock Object library for PHPUnit
psr/container                            1.0.0              Common Container Interface (PHP FIG PSR-11)
psr/http-message                         1.0.1              Common interface for HTTP messages
roave/security-advisories                dev-master 664836e Prevents installation of composer packages with known security vulnerabilities: no API, simply require it
sebastian/code-unit-reverse-lookup       1.0.1              Looks up which function or method a line of code belongs to
sebastian/comparator                     2.1.3              Provides the functionality to compare PHP values for equality
sebastian/diff                           2.0.1              Diff implementation
sebastian/environment                    3.1.0              Provides functionality to handle HHVM/PHP environments
sebastian/exporter                       3.1.0              Provides the functionality to export PHP variables for visualization
sebastian/global-state                   2.0.0              Snapshotting of global state
sebastian/object-enumerator              3.0.3              Traverses array structures and object graphs to enumerate all referenced objects
sebastian/object-reflector               1.1.1              Allows reflection of object attributes, including inherited and non-public ones
sebastian/recursion-context              3.0.0              Provides functionality to recursively process PHP variables
sebastian/resource-operations            1.0.0              Provides a list of PHP built-in functions that operate on resources
sebastian/version                        2.0.1              Library that helps with managing the version number of Git-hosted PHP projects
squizlabs/php_codesniffer                2.9.1              PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding...
symfony/console                          v4.0.6             Symfony Console Component
symfony/polyfill-mbstring                v1.7.0             Symfony polyfill for the Mbstring extension
theseer/tokenizer                        1.1.0              A small library for converting tokenized PHP source code into XML and potentially other formats
webimpress/composer-extra-dependency     0.2.2              Composer plugin to require extra dependencies
webimpress/http-middleware-compatibility 0.1.4              Compatibility library for Draft PSR-15 HTTP Middleware
webmozart/assert                         1.3.0              Assertions to validate method input/output with nice error messages.
zendframework/zend-code                  3.3.0              provides facilities to generate arbitrary code using an object oriented interface
zendframework/zend-component-installer   1.1.1              Composer plugin for automating component registration in zend-mvc and Expressive applications
zendframework/zend-config-aggregator     1.1.0              Lightweight library for collecting and merging configuration from different sources
zendframework/zend-diactoros             1.7.1              PSR HTTP Message implementations
zendframework/zend-escaper               2.5.2             
zendframework/zend-eventmanager          3.2.0              Trigger and listen to events within a PHP application
zendframework/zend-expressive            2.1.0              PSR-7 Middleware Microframework based on Stratigility
zendframework/zend-expressive-helpers    4.2.0              Helper/Utility classes for Expressive
zendframework/zend-expressive-router     2.4.1              Router subcomponent for Expressive
zendframework/zend-expressive-template   1.0.4              Template subcomponent for Expressive
zendframework/zend-expressive-tooling    0.4.6              Migration and development tooling for Expressive
zendframework/zend-expressive-zendrouter 2.2.0              zend-mvc router support for Expressive
zendframework/zend-http                  2.7.0              provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests
zendframework/zend-loader                2.5.1             
zendframework/zend-psr7bridge            1.0.2              PSR-7 &lt;-&gt; zend-http message conversions
zendframework/zend-router                3.0.2             
zendframework/zend-servicemanager        3.3.2              Factory-Driven Dependency Injection Container
zendframework/zend-stdlib                3.1.0             
zendframework/zend-stratigility          2.1.2              Middleware for PHP
zendframework/zend-uri                   2.5.2              a component that aids in manipulating and validating
zendframework/zend-validator             2.10.2             provides a set of commonly needed validators
zfcampus/zf-composer-autoloading         2.0.0              Sets up Composer-based autoloading for your Zend Framework modules
zfcampus/zf-development-mode             3.1.0              Zend Framework development mode script

If I use the programmatic way of injecting pipeline and routes through the files pipeline.php and routes.php all works, but the moment I use the method $app->injectRoutesFromConfig($container->get('config')); inside an application delegator this is the error that I see:

Deprecated: Zend\Expressive\Router\Route will not accept anything other than objects implementing the MiddlewareInterface starting in version 3.0.0; we detected usage of "array" for path "/api/ping" using methods GET. Please update your code to create middleware instances implementing MiddlewareInterface; use decorators for callable middleware if needed. in /var/www/html/vendor/zendframework/zend-expressive-router/src/Route.php on line 296

This is the ConfigProvider.php:

public function getRoutesConfig() {
		return [
			[
				'name' => 'api.ping',
				'path' => '/api/ping',
				'middleware' => [
					Action\PingAction::class
				],
				'allowed_methods' => ['GET']
			]
		];
	}

This is the ApplicationDelegator.php:

<?php

/**
 * container-interop-doctrine
 *
 * @link      http://github.com/DASPRiD/container-interop-doctrine For the canonical source repository
 * @copyright 2016 Ben Scholzen 'DASPRiD'
 * @license   http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
 */

namespace App;

use Psr\Container\ContainerInterface;
use Zend\Expressive\Helper\ServerUrlMiddleware;
use Zend\Expressive\Helper\UrlHelperMiddleware;
use Zend\Expressive\Middleware\ImplicitHeadMiddleware;
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;
use Zend\Expressive\Middleware\NotFoundHandler;
use Zend\Stratigility\Middleware\ErrorHandler;

class ApplicationDelegatorFactory {

	/**
	 * @param ContainerInterface $container
	 * @param string $serviceName Name of the service being created.
	 * @param callable $callback Creates and returns the service.
	 * @return Application
	 */
	public function __invoke(ContainerInterface $container, $serviceName, callable $callback) {

		/** @var $app Application */
		$app = $callback();

		/**
		 * Setup middleware pipeline:
		 */
		/** @var \Zend\Expressive\Application $app */
// The error handler should be the first (most outer) middleware to catch
// all Exceptions.
		$app->pipe(\Blast\BaseUrl\BaseUrlMiddleware::class);
		$app->pipe(ErrorHandler::class);
		$app->pipe(ServerUrlMiddleware::class);

// Pipe more middleware here that you want to execute on every request:
// - bootstrapping
// - pre-conditions
// - modifications to outgoing responses
//
// Piped Middleware may be either callables or service names. Middleware may
// also be passed as an array; each item in the array must resolve to
// middleware eventually (i.e., callable or service name).
//
// Middleware can be attached to specific paths, allowing you to mix and match
// applications under a common domain.  The handlers in each middleware
// attached this way will see a URI with the MATCHED PATH SEGMENT REMOVED!!!
//
// - $app->pipe('/api', $apiMiddleware);
// - $app->pipe('/docs', $apiDocMiddleware);
// - $app->pipe('/files', $filesMiddleware);
// Register the routing middleware in the middleware pipeline
		$app->pipeRoutingMiddleware();
		$app->pipe(ImplicitHeadMiddleware::class);
		$app->pipe(ImplicitOptionsMiddleware::class);
		$app->pipe(UrlHelperMiddleware::class);

// Add more middleware here that needs to introspect the routing results; this
// might include:
//
// - route-based authentication
// - route-based validation
// - etc.
// Register the dispatch middleware in the middleware pipeline
		$app->pipeDispatchMiddleware();

// At this point, if no Response is returned by any middleware, the
// NotFoundHandler kicks in; alternately, you can provide other fallback
// middleware to execute.
		$app->pipe(NotFoundHandler::class);

		/**
		 * Setup routes with a single request method:
		 */
		$app->injectRoutesFromConfig($container->get('config'));

		return $app;
	}

}

Hello @dominicdettabp!

So it looks like the problem is with config driven configuration.
Here is my proposed fix for expressive:

It’s WIP and needs some improvements, but you can check these changes with your application. It works for me fine :slight_smile:

@michalbundyra Hellooooo I tried your fix and… IT WORKS!!! :star_struck:
I’m looking forward for the fix to be merged in the zendframework/zend-expressive repository

@dominicdettabp not sure if you noticed, but zend-expressive 2.1.1 with the hotfix is already released.

@michalbundyra Yep, noticed and updated my project. Thanks.
One question: When it will be available Zend expressive 2.2 with the fix on zend-expressive-router ?
I ask because I want to be able to use the OPTIONS request.

The fix is present in zend-expressive 2.1.1.

zend-expressive 2.2 will also contain it, but with the exact same changes, essentially. That will release today or tomorrow.

zend-expressive v3 has a more comprehensive solution, but one that required backwards compatibility breaks (which is why it’s not in the v2 series); that releases on Thursday.