Skip to content

Instantly share code, notes, and snippets.

@tachyondecay
Last active February 12, 2024 06:11
Show Gist options
  • Save tachyondecay/a17960d5d8276387e7cf6982760e7ec2 to your computer and use it in GitHub Desktop.
Save tachyondecay/a17960d5d8276387e7cf6982760e7ec2 to your computer and use it in GitHub Desktop.
Host matching in blueprints at reg time + free static endpoint creation

What This Does

Let's say you want to serve 2 domains, example1.com and example2.com, from the same app (i.e., using host matching). You also want your static assets for each domain to be served from those domains, e.g., example1.com/static/ and example2.com/static/. You can almost achieve this in Flask, but not quite.

This code extends Flask's Blueprint class to make registering hostnames onto a blueprint painless and ensure static assets appear to be served from the appropriate hostname.

Default Flask Behaviour

By default, Flask supports host matching at the app-level by passing host_matching=True and a hostname for static_host when initializing the app. However, there are drawbacks to the current support, especially when using blueprints:

  1. To match all routes on a blueprint to a host, you must pass the host parameter to every route decorator for that blueprint.
  2. The app's static files will always be served from the static_host, and trying to override the blueprint static_folder produces some undefined behaviour. Even when you try to override static_url_path using a secondary hostname, at best you'll be stuck with absolute paths.

What This Accomplishes

With the above Blueprint and BlueprintSetupState classes, you can do the following:

  • Register a host parameter at blueprint registration time. This applies the parameter to every route on that blueprint without anything extra in your code. It works with url_prefix
  • Automatically get a blueprint static endpoint that generates a URL relative to the blueprint's hostname. By default, this will map to the app static folder—i.e., /static/foo.jpg would be available to be served by both localhost.foo:5000/foo.jpg and localhost.bar:5000/bar.jpg. So, there is no actual separation of the static assets. If you want to separate your assets, you can still pass static_folder when instantiating the Blueprint option. Again, this will result in relative static URL paths being generated.
from flask.blueprints import (
Blueprint as FlaskBlueprint,
BlueprintSetupState as FlaskBlueprintSetupState,
)
class BlueprintSetupState(FlaskBlueprintSetupState):
"""Adds the ability to set a host matching parameter on all routes when registering the blueprint."""
def __init__(self, blueprint, app, options, first_registration):
super().__init__(blueprint, app, options, first_registration)
host = self.options.get("host")
if host is None:
host = self.blueprint.host
self.host = host
# This creates a 'blueprint_name.static' endpoint.
# The location of the static folder is shared with the app static folder,
# but all static resources will be served via the blueprint's hostname.
if app.url_map.host_matching and not self.blueprint.has_static_folder:
url_prefix = self.url_prefix
self.url_prefix = None
self.add_url_rule(
f"{app.static_url_path}/<path:filename>",
view_func=app.send_static_file,
endpoint="static",
)
self.url_prefix = url_prefix
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
# Ensure that every route registered by this blueprint has the host parameter
options.setdefault('host', self.host)
super().add_url_rule(rule, endpoint, view_func, **options)
class Blueprint(FlaskBlueprint):
def make_setup_state(self, app, options, first_registration=False):
return BlueprintSetupState(self, app, options, first_registration)
from flask import Flask, url_for
from .blueprints import Blueprint
bp_foo = Blueprint('foo', __name__)
bp_bar = Blueprint('bar', __name__)
@bp_foo.route("/")
def hello():
# Outputs /static/foo.jpg
return url_for("foo.static", filename="foo.jpg")
@bp_bar.route("/")
def world():
# Outputs /static/bar.jpg
return url_for("bar.static", filename="bar.jpg")
def create_app():
app = Flask(__name__, static_host="localhost.foo:5000", host_matching=True)
app.register_blueprint(bp_foo, host="localhost.foo:5000", url_prefix='/')
app.register_blueprint(bp_bar, host="localhost.bar:5000", url_prefix='/')
return app
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment