Skip to content

Commit 4e0502d

Browse files
authored
Merge pull request #1289 from plotly/startup-message
DASH_PROXY
2 parents 526af0c + 1cbbb58 commit 4e0502d

File tree

4 files changed

+120
-20
lines changed

4 files changed

+120
-20
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [UNRELEASED]
66
### Added
7+
- [#1289](https://github.com/plotly/dash/pull/1289) Supports `DASH_PROXY` env var to tell `app.run_server` to report the correct URL to view your app, when it's being proxied. Throws an error if the proxy is incompatible with the host and port you've given the server.
78
- [#1240](https://github.com/plotly/dash/pull/1240) Adds `callback_context` to clientside callbacks (e.g. `dash_clientside.callback_context.triggered`). Supports `triggered`, `inputs`, `inputs_list`, `states`, and `states_list`, all of which closely resemble their serverside cousins.
89

910
### Changed

dash/dash.py

+55-20
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from __future__ import print_function
22

3-
import itertools
43
import os
5-
import random
64
import sys
75
import collections
86
import importlib
@@ -14,6 +12,7 @@
1412
import mimetypes
1513

1614
from functools import wraps
15+
from future.moves.urllib.parse import urlparse
1716

1817
import flask
1918
from flask_compress import Compress
@@ -25,7 +24,7 @@
2524
from .fingerprint import build_fingerprint, check_fingerprint
2625
from .resources import Scripts, Css
2726
from .development.base_component import ComponentRegistry
28-
from .exceptions import PreventUpdate, InvalidResourceError
27+
from .exceptions import PreventUpdate, InvalidResourceError, ProxyError
2928
from .version import __version__
3029
from ._configs import get_combined_config, pathname_configs
3130
from ._utils import (
@@ -1332,7 +1331,8 @@ def enable_dev_tools(
13321331

13331332
if dev_tools.silence_routes_logging:
13341333
logging.getLogger("werkzeug").setLevel(logging.ERROR)
1335-
self.logger.setLevel(logging.INFO)
1334+
1335+
self.logger.setLevel(logging.INFO)
13361336

13371337
if dev_tools.hot_reload:
13381338
_reload = self._hot_reload
@@ -1449,6 +1449,7 @@ def run_server(
14491449
self,
14501450
host=os.getenv("HOST", "127.0.0.1"),
14511451
port=os.getenv("PORT", "8050"),
1452+
proxy=os.getenv("DASH_PROXY", None),
14521453
debug=False,
14531454
dev_tools_ui=None,
14541455
dev_tools_props_check=None,
@@ -1475,6 +1476,14 @@ def run_server(
14751476
env: ``PORT``
14761477
:type port: int
14771478
1479+
:param proxy: If this application will be served to a different URL
1480+
via a proxy configured outside of Python, you can list it here
1481+
as a string of the form ``"{input}::{output}"``, for example:
1482+
``"http://0.0.0.0:8050::https://my.domain.com"``
1483+
so that the startup message will display an accurate URL.
1484+
env: ``DASH_PROXY``
1485+
:type proxy: string
1486+
14781487
:param debug: Set Flask debug mode and enable dev tools.
14791488
env: ``DASH_DEBUG``
14801489
:type debug: bool
@@ -1555,25 +1564,51 @@ def run_server(
15551564
]
15561565
raise
15571566

1558-
if self._dev_tools.silence_routes_logging:
1559-
# Since it's silenced, the address doesn't show anymore.
1567+
# so we only see the "Running on" message once with hot reloading
1568+
# https://stackoverflow.com/a/57231282/9188800
1569+
if os.getenv("WERKZEUG_RUN_MAIN") != "true":
15601570
ssl_context = flask_run_options.get("ssl_context")
1561-
self.logger.info(
1562-
"Running on %s://%s:%s%s",
1563-
"https" if ssl_context else "http",
1564-
host,
1565-
port,
1566-
self.config.requests_pathname_prefix,
1567-
)
1571+
protocol = "https" if ssl_context else "http"
1572+
path = self.config.requests_pathname_prefix
1573+
1574+
if proxy:
1575+
served_url, proxied_url = map(urlparse, proxy.split("::"))
1576+
1577+
def verify_url_part(served_part, url_part, part_name):
1578+
if served_part != url_part:
1579+
raise ProxyError(
1580+
"""
1581+
{0}: {1} is incompatible with the proxy:
1582+
{3}
1583+
To see your app at {4},
1584+
you must use {0}: {2}
1585+
""".format(
1586+
part_name,
1587+
url_part,
1588+
served_part,
1589+
proxy,
1590+
proxied_url.geturl(),
1591+
)
1592+
)
1593+
1594+
verify_url_part(served_url.scheme, protocol, "protocol")
1595+
verify_url_part(served_url.hostname, host, "host")
1596+
verify_url_part(served_url.port, port, "port")
15681597

1569-
# Generate a debugger pin and log it to the screen.
1570-
debugger_pin = os.environ["WERKZEUG_DEBUG_PIN"] = "-".join(
1571-
itertools.chain(
1572-
"".join([str(random.randint(0, 9)) for _ in range(3)])
1573-
for _ in range(3)
1598+
display_url = (
1599+
proxied_url.scheme,
1600+
proxied_url.hostname,
1601+
(":{}".format(proxied_url.port) if proxied_url.port else ""),
1602+
path,
15741603
)
1575-
)
1604+
else:
1605+
display_url = (protocol, host, ":{}".format(port), path)
1606+
1607+
self.logger.info("Dash is running on %s://%s%s%s\n", *display_url)
1608+
self.logger.info(" Warning: This is a development server. Do not use app.run_server")
1609+
self.logger.info(" in production, use a production WSGI server like gunicorn instead.\n")
15761610

1577-
self.logger.info("Debugger PIN: %s", debugger_pin)
1611+
if not os.environ.get("FLASK_ENV"):
1612+
os.environ["FLASK_ENV"] = "development"
15781613

15791614
self.server.run(host=host, port=port, debug=debug, **flask_run_options)

dash/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ class MissingCallbackContextException(CallbackException):
7373

7474
class UnsupportedRelativePath(CallbackException):
7575
pass
76+
77+
78+
class ProxyError(DashException):
79+
pass

tests/unit/test_configs.py

+60
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,63 @@ def test_port_env_fail_range(empty_environ):
254254
excinfo.exconly()
255255
== "AssertionError: Expecting an integer from 1 to 65535, found port=65536"
256256
)
257+
258+
259+
def test_no_proxy_success(mocker, caplog, empty_environ):
260+
app = Dash()
261+
262+
# mock out the run method so we don't actually start listening forever
263+
mocker.patch.object(app.server, "run")
264+
265+
app.run_server(port=8787)
266+
267+
assert "Dash is running on http://127.0.0.1:8787/\n" in caplog.text
268+
269+
270+
@pytest.mark.parametrize(
271+
"proxy, host, port, path",
272+
[
273+
("https://daash.plot.ly", "127.0.0.1", 8050, "/"),
274+
("https://daaash.plot.ly", "0.0.0.0", 8050, "/a/b/c/"),
275+
("https://daaaash.plot.ly", "127.0.0.1", 1234, "/"),
276+
("http://go.away", "127.0.0.1", 8050, "/now/"),
277+
("http://my.server.tv:8765", "0.0.0.0", 80, "/"),
278+
],
279+
)
280+
def test_proxy_success(mocker, caplog, empty_environ, proxy, host, port, path):
281+
proxystr = "http://{}:{}::{}".format(host, port, proxy)
282+
app = Dash(url_base_pathname=path)
283+
mocker.patch.object(app.server, "run")
284+
285+
app.run_server(proxy=proxystr, host=host, port=port)
286+
287+
assert "Dash is running on {}{}\n".format(proxy, path) in caplog.text
288+
289+
290+
def test_proxy_failure(mocker, empty_environ):
291+
app = Dash()
292+
293+
# if the tests work we'll never get to server.run, but keep the mock
294+
# in case something is amiss and we don't get an exception.
295+
mocker.patch.object(app.server, "run")
296+
297+
with pytest.raises(_exc.ProxyError) as excinfo:
298+
app.run_server(
299+
proxy="https://127.0.0.1:8055::http://plot.ly", host="127.0.0.1", port=8055
300+
)
301+
assert "protocol: http is incompatible with the proxy" in excinfo.exconly()
302+
assert "you must use protocol: https" in excinfo.exconly()
303+
304+
with pytest.raises(_exc.ProxyError) as excinfo:
305+
app.run_server(
306+
proxy="http://0.0.0.0:8055::http://plot.ly", host="127.0.0.1", port=8055
307+
)
308+
assert "host: 127.0.0.1 is incompatible with the proxy" in excinfo.exconly()
309+
assert "you must use host: 0.0.0.0" in excinfo.exconly()
310+
311+
with pytest.raises(_exc.ProxyError) as excinfo:
312+
app.run_server(
313+
proxy="http://0.0.0.0:8155::http://plot.ly", host="0.0.0.0", port=8055
314+
)
315+
assert "port: 8055 is incompatible with the proxy" in excinfo.exconly()
316+
assert "you must use port: 8155" in excinfo.exconly()

0 commit comments

Comments
 (0)