Horizon

Share a Flask app

Share a Flask app from flask run with a Horizon tunnel, and make url_for build the public https address with Werkzeug ProxyFix.

Share your Flask app from flask run with a public HTTPS URL, with generated links pointing at the tunnel.

Before you begin

  • Node.js 18 or later
  • A Horizon account and the CLI (see Getting started)
  • A reserved subdomain for -s. Reserve one on the Subdomains page.
  • A Flask app you can run with flask run

Start the dev server

flask run listens on port 5000 by default.

flask run

Start a tunnel

Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run. Reserved subdomains are a paid feature, see Pricing.

hrzn tunnel http://localhost:5000 -s my-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:5000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Your public URL is https://my-app.hrzn.run. Keep this terminal open.

Use the public host and scheme

The CLI sets Host to localhost:5000. Without changes, url_for(..., _external=True) and request.url_root return http://localhost:5000/. The public host arrives in X-Forwarded-Host.

Wrap the app in Werkzeug's ProxyFix with x_host=1 to read that header. Don't set x_proto. Horizon appends to X-Forwarded-Proto, so the app can receive https,http, and ProxyFix reads the last value, http. Set the scheme yourself instead.

app.py
from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)


class ForceHttps:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        environ["wsgi.url_scheme"] = "https"
        return self.wsgi_app(environ, start_response)


app.wsgi_app = ProxyFix(ForceHttps(app.wsgi_app), x_host=1)

ForceHttps is for local sharing. Remove both wrappers when you deploy, and configure your real proxy instead. Restart flask run after you edit the file.

Check it works

Add a route that prints the URL Flask generates.

app.py
from flask import url_for


@app.get("/where")
def where():
    return url_for("where", _external=True)

Open https://my-app.hrzn.run/where. On the first visit Horizon shows a Before you continue page. Select Continue to site. The page prints https://my-app.hrzn.run/where. The Horizon terminal prints:

Output
  GET     200  /where

Troubleshooting

ProxyFix isn't active, or x_host=1 is missing. Check that app.wsgi_app is wrapped, and restart flask run.

The ForceHttps wrapper is missing, or ProxyFix wraps it from the inside. Wrap in this order: ProxyFix(ForceHttps(app.wsgi_app), x_host=1).

The URL changed after a restart

You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.

Nothing reaches your app

  • Check that the Horizon terminal is still running. If its last line is Connection lost. Reconnecting…, wait for Reconnected.
  • Check that flask run listens on port 5000, the port in your hrzn tunnel command.

Next steps

On this page