URL routing conflicts for static files in Flask dev server

This is werkzeug route optimization feature. See Map.add, Map.update and Rule.match_compare_key:

def match_compare_key(self):
    """The match compare key for sorting.

    Current implementation:

    1. rules without any arguments come first for performance
    reasons only as we expect them to match faster and some
    common ones usually don't have any arguments (index pages etc.)
    2. The more complex rules come first so the second argument is the
    negative length of the number of weights.
    3. lastly we order by the actual weights.

    :internal:
    """
    return bool(self.arguments), -len(self._weights), self._weights

There are self.arguments - current arguments, self._weights - path depth.

For '/<var_1>/<var_2>/<var3>/' we have (True, -3, [(1, 100), (1, 100), (1, 100)]). There are (1, 100) - default string argument with max length 100.

For '/static/<path:filename>' we have (True, -2, [(0, -6), (1, 200)]). There are (0, 1) - path non argument string length static, (1, 200) - path string argument max length 200.

So I don't find any beautiful way to set own Map implementation for Flask.url_map or set priority for map rule. Solutions:

  1. Setup Flask application as app = Flask(static_path='static', static_url_path='/more/then/your/max/variables/path/depth/static').
  2. Change @app.route('/<var_1>/<var_2>/<var3>/') to @app.route('/prefix/<var_1>/<var_2>/<var3>/').
  3. Add own converter and use as @app.route('/<no_static:var_1>/<var_2>/<var3>/').
  4. Import werkzeug.routing, create own map implementation, change werkzeug.routing.Map to own implementation, import flask.
  5. Use server as on production.

So, as tbicr pointed out, this behavior is set deep within Werkzeug, and there's not really an elegant way to handle it from Flask. The best workaround I could come up with is:

Define a complementary static file handler like:

@app.route('/static/<subdir>/<path:filename>/')
def static_subdir(subdir=None, filename=None):

    directory = app.config['STATIC_FOLDER'] + subdir
    return send_from_directory(directory, filename)

Here, app.config['STATIC_FOLDER'] is the full path to the static folder on the machine running the application.

Now, this handler catches things like /static/images/img.jpg, leaving my view with the three variable components alone.


One way to go around this is to cheat the rules sorting algorithm by spoofing the registered rule's match_compare_key() method. Note that this hack only works with routes that have been registered directly with app.route() (the Flask object), not with Blueprints. Blueprints' routes are added to the global url Map only upon blueprint's registration on the main app, making it challenging to modify the generated rules.

# an ordinary route
@app.route('/<var1>/<var2>/<var3>')
def some_view(var1, var2, var3):
    pass

# let's find the rule that was just generated
rule = app.url_map._rules[-1]

# we create some comparison keys:
# increase probability that the rule will be near or at the top
top_compare_key = False, -100, [(-2, 0)]
# increase probability that the rule will be near or at the bottom 
bottom_compare_key = True, 100, [(2, 0)]

# rig rule.match_compare_key() to return the spoofed compare_key
rule.match_compare_key = lambda: top_compare_key

Note that in this case the resulting spoofed function is not bound to the rule object. Therefore upon calling rule.match_compare_key(), the function does not receive a self argument. If you want to bind the function properly, do this instead:

spoof = lambda self: top_compare_key
rule.match_compare_key = spoof.__get__(rule, type(rule))

We can generalize the above with a decorator

def weighted_route(*args, **kwargs):
    def decorator(view_func):
        compare_key = kwargs.pop('compare_key', None)
        # register view_func with route
        app.route(*args, **kwargs)(view_func)

        if compare_key is not None:
            rule = app.url_map._rules[-1]
            rule.match_compare_key = lambda: compare_key

        return view_func
    return decorator

# can be used like @app.route(). To weight the rule, just provide
# the `compare_key` param.
@weighted_route('/<var1>/<var2>/<var3>', compare_key=bottom_compare_key)
def some_view(var1, var2, var3):
    pass

The same hack implemented as a context manager.

import contextlib

@contextlib.contextmanager
def weighted_route(compare_key=None):
    yield
    if compare_key is not None:
        rule = app.url_map._rules[-1]
        rule.match_compare_key = lambda: compare_key

# and to use

with weighted_route(compare_key):
    @app.route('/<var1>/<var2>/<var3>')
    def some_view(var1, var2, var3):
        pass