Slim v3 CORS dynamic OPTIONS routes


#1

I’d like to dynamically create an OPTIONS route for each routing pattern so that I can more accurately provide an Access-Control-Allowed-Methods header. Slim already returns 200 for matching routes or 405 for non-matching routes in response to an OPTIONS request. It also returns an Allow header, which does not include OPTIONS.

I have middleware successfully returning an Access-Control-Allowed-Methods header with the list of methods that are defined for each route pattern, and can dynamically push OPTIONS onto that list, but I want to do this properly. I don’t want to have to manually add an OPTIONS map and conditionally execute my PUT/PATCH/DELETE logic based on the method.

I’d like to be able to dynamically define an OPTIONS route for each extant pattern (no matter the number of methods). I made a start, but I can’t access $app to call options() or map() from within my middleware:

$app->add(function($request, $response, $next) use ($app) {
    foreach ($this->router->getRoutes() as $route) {
            $pattern = $route->getPattern();
            $methods = $route->getMethods();
            if(!in_array('OPTIONS', $methods)) {
                $app->map(['OPTIONS'],$pattern, function ($request, $response, $args) {
                $response->setStatus(204);
            });
        }
    }
    return $next($request, $response);
});

Any tips?


#2

Welcome @Sam_Butler :slight_smile:

If I have understood you correctly, you want to add a specific middleware to each route.
In Slim it’s possible to add middleware to any route or group of routes.
Have you tried this already?


#3

If I understand correctly, you just want to for each route you define OPTIONS alternative
exmaple:
you want define
GET /v1/getUser
by
$app->get('v1/getUser', function(){});

and automatically you want also “register”
OPTIONS /v1/getUser

I would probable use Slim\App class overload:

<?php

namespace MyApp\Slim;

class App extends \Slim\App {

/**
 * @inheritdoc
 */
public function map(array $methods, $pattern, $callable)
{
	if (!in_array('OPTIONS', $methods)) {
		$methods[] = 'OPTIONS';
	}

	return parent::map($methods, $pattern, $callable);
}

}

obviously than you need to use MyApp\Slim\App() instance of Slim App before routes setup

note that there are some gotchas but I think this might simple way how to solve your problem?


#4

sorry for typos in text, there is no way how to fix message typos here…lol ("message body is too similar to previous content…wtf is that)


#5

Thanks @odan - not exactly, because I need to map the OPTIONS method to each route that doesn’t have it defined. I assume since it is global to all routes, I would need to determine route patterns from application middleware. @jdolba’s answer looks more like it, I’m going to have a go with that.


#6

Nearly there. Extending in this way does let me add the method to each route, however where we define two (or more) methods separately for the same pattern, this will attempt to add OPTIONS to each, resulting in a fatal error in FastRoute - cannot register two routes matching X for method “OPTIONS”. I’ll keep looking but let me know if you have any ideas.


#7

Additionally, this doesn’t define the output separately, so even with my modifications it would map OPTIONS to the first defined route for a given pattern. If there was only a DELETE that might mean we execute the deletion logic, and would need some conditional logic in every route’s anonymous function. So what I think we need is to recursively call map() rather than simply adding to the list of methods.


#8

you can check option
‘determineRouteBeforeAppMiddleware’

then add one middlewere which will check request method (as it will call middleware only if route is matched) and if it will be OPTIONS you can do your stuff


#9

Back to square one - I’d already done that. Effectively the problem is that when it’s an OPTIONS request, because the pattern+method doesn’t match a defined route, I’m getting nothing on $request->getAttribute("route"); and without that I can’t do $route->getPattern() which is important because you can’t get that from the URL - the pattern includes variables and expressions. I need the pattern to search other matching routes and get the list of methods. You see my problem?


#10

According to https://github.com/nikic/FastRoute/pull/168 nikic/FastRoute added support for an OPTIONS convenience method a few months ago. I do get an Allow header back from all defined routes with OPTIONS without my middleware, and a 404 if the pattern doesn’t match any defined routes. Perhaps my integration needs to go deeper than Slim, because the Allow header already has what I want but I want to (also) output it as Access-Control-Allow-Methods.


#11

If you use apache, this could be realized with a small tweak in the .htacess file. But have no example at the moment. Maybe just Google “cors htacess” and you will find some examples.


#12

To clarify, it’s very easy to produce a catch-all OPTIONS response with CORS headers for every conceivable method across all endpoints.

What I wanted to do was to dynamically add an OPTIONS route for every defined pattern and return a list of actual methods that are allowed for that endpoint.

For example, if my pattern is /api/users/{id} and I have routes for GET, POST, PUT and PATCH then I want to be able to return Access-Control-Allow-Methods: GET, POST, PUT, PATCH, OPTIONS. And if I didn’t define PUT then I don’t want it in the list, or if I defined DELETE then I do. I don’t want to have to work around the issue by mapping OPTIONS to my PUT because what if I have a PUT and a DELETE, where do I map my OPTIONS? Or no PUT - how can I be consistent? You can’t map OPTIONS twice. And then I need conditional logic to check if it’s OPTIONS and not execute the rest of my code.

I can already get the list of allowed methods into a defined route, so if I separately define an OPTIONS route for each pattern I map to another method, my middleware will take care of that. If I don’t, the server will still respond on OPTIONS but I have no control over what it returns. To dynamically define those OPTIONS however I need access to the current instance of \Slim\App within my middleware.

In v2 we used to have a getInstance static method. Does anyone know how I might achieve this in v3?