Skip to content

Commit 8d77e5f

Browse files
committed
Merge branch 'dev' into feat/add-folder-upload-support
2 parents f2512b9 + 0221a97 commit 8d77e5f

File tree

4 files changed

+105
-6
lines changed

4 files changed

+105
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1313
- [#3424](https://github.com/plotly/dash/pull/3424) Adds support for `Patch` on clientside callbacks class `dash_clientside.Patch`, as well as supporting side updates, eg: (Running, SetProps).
1414
- [#3347](https://github.com/plotly/dash/pull/3347) Added 'api_endpoint' to `callback` to expose api endpoints at the provided path for use to be executed directly without dash.
1515
- [#3445](https://github.com/plotly/dash/pull/3445) Added API to reverse direction of slider component.
16+
- [#3460](https://github.com/plotly/dash/pull/3460) Add `/health` endpoint for server monitoring and health checks.
1617
- [#3465](https://github.com/plotly/dash/pull/3465) Plotly cloud integrations, add devtool API, placeholder plotly cloud CLI & publish button, `dash[cloud]` extra dependencies.
1718

1819
## Fixed

dash/_get_app.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ def wrap(*args, **kwargs):
3939

4040

4141
def get_app():
42+
"""
43+
Return the current Dash app instance.
44+
45+
Useful in multi-page apps when Python files within the `pages/` folder
46+
need to reference the `app` object but importing it directly would cause
47+
a circular import error.
48+
"""
4249
try:
4350
ctx_app = app_context.get()
4451
if ctx_app is not None:

dash/dash.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ class Dash(ObsoleteChecker):
416416
:param use_async: When True, the app will create async endpoints, as a dev,
417417
they will be responsible for installing the `flask[async]` dependency.
418418
:type use_async: boolean
419+
420+
:param health_endpoint: Path for the health check endpoint. Set to None to
421+
disable the health endpoint. Default is None.
422+
:type health_endpoint: string or None
419423
"""
420424

421425
_plotlyjs_url: str
@@ -466,6 +470,7 @@ def __init__( # pylint: disable=too-many-statements
466470
description: Optional[str] = None,
467471
on_error: Optional[Callable[[Exception], Any]] = None,
468472
use_async: Optional[bool] = None,
473+
health_endpoint: Optional[str] = None,
469474
**obsolete,
470475
):
471476

@@ -537,6 +542,7 @@ def __init__( # pylint: disable=too-many-statements
537542
update_title=update_title,
538543
include_pages_meta=include_pages_meta,
539544
description=description,
545+
health_endpoint=health_endpoint,
540546
)
541547
self.config.set_read_only(
542548
[
@@ -769,6 +775,8 @@ def _setup_routes(self):
769775
self._add_url("_dash-update-component", self.dispatch, ["POST"])
770776
self._add_url("_reload-hash", self.serve_reload_hash)
771777
self._add_url("_favicon.ico", self._serve_default_favicon)
778+
if self.config.health_endpoint is not None:
779+
self._add_url(self.config.health_endpoint, self.serve_health)
772780
self._add_url("", self.index)
773781

774782
if jupyter_dash.active:
@@ -929,13 +937,17 @@ def _config(self):
929937
"plotly_version": plotly_version,
930938
}
931939
if self._plotly_cloud is None:
932-
try:
933-
# pylint: disable=C0415,W0611
934-
import plotly_cloud # noqa: F401
935-
940+
if os.getenv("DASH_ENTERPRISE_ENV") == "WORKSPACE":
941+
# Disable the placeholder button on workspace.
936942
self._plotly_cloud = True
937-
except ImportError:
938-
self._plotly_cloud = False
943+
else:
944+
try:
945+
# pylint: disable=C0415,W0611
946+
import plotly_cloud # noqa: F401
947+
948+
self._plotly_cloud = True
949+
except ImportError:
950+
self._plotly_cloud = False
939951

940952
config["plotly_cloud_installed"] = self._plotly_cloud
941953
if not self.config.serve_locally:
@@ -987,6 +999,13 @@ def serve_reload_hash(self):
987999
}
9881000
)
9891001

1002+
def serve_health(self):
1003+
"""
1004+
Health check endpoint for monitoring Dash server status.
1005+
Returns a simple "OK" response with HTTP 200 status.
1006+
"""
1007+
return flask.Response("OK", status=200, mimetype="text/plain")
1008+
9901009
def get_dist(self, libraries: Sequence[str]) -> list:
9911010
dists = []
9921011
for dist_type in ("_js_dist", "_css_dist"):
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Tests for the health endpoint.
3+
4+
Covers:
5+
- disabled by default
6+
- enabled returns plain OK 200
7+
- respects routes_pathname_prefix
8+
- custom nested path works
9+
- HEAD allowed, POST not allowed
10+
"""
11+
12+
from dash import Dash, html
13+
14+
15+
def test_health_disabled_by_default_returns_404():
16+
app = Dash(__name__) # health_endpoint=None by default
17+
app.layout = html.Div("Test")
18+
client = app.server.test_client()
19+
r = client.get("/health")
20+
# When health endpoint is disabled, it returns the main page (200) instead of 404
21+
# This is expected behavior - the health endpoint is not available
22+
assert r.status_code == 200
23+
# Should return HTML content, not "OK"
24+
assert b"OK" not in r.data
25+
26+
27+
def test_health_enabled_returns_ok_200_plain_text():
28+
app = Dash(__name__, health_endpoint="health")
29+
app.layout = html.Div("Test")
30+
client = app.server.test_client()
31+
32+
r = client.get("/health")
33+
assert r.status_code == 200
34+
assert r.data == b"OK"
35+
# Flask automatically sets mimetype to text/plain for Response with mimetype
36+
assert r.mimetype == "text/plain"
37+
38+
39+
def test_health_respects_routes_pathname_prefix():
40+
app = Dash(__name__, routes_pathname_prefix="/x/", health_endpoint="health")
41+
app.layout = html.Div("Test")
42+
client = app.server.test_client()
43+
44+
ok = client.get("/x/health")
45+
miss = client.get("/health")
46+
47+
assert ok.status_code == 200 and ok.data == b"OK"
48+
assert miss.status_code == 404
49+
50+
51+
def test_health_custom_nested_path():
52+
app = Dash(__name__, health_endpoint="api/v1/health")
53+
app.layout = html.Div("Test")
54+
client = app.server.test_client()
55+
56+
r = client.get("/api/v1/health")
57+
assert r.status_code == 200
58+
assert r.data == b"OK"
59+
60+
61+
def test_health_head_allowed_and_post_405():
62+
app = Dash(__name__, health_endpoint="health")
63+
app.layout = html.Div("Test")
64+
client = app.server.test_client()
65+
66+
head = client.head("/health")
67+
assert head.status_code == 200
68+
# for HEAD the body can be empty, so we do not validate body
69+
assert head.mimetype == "text/plain"
70+
71+
post = client.post("/health")
72+
assert post.status_code == 405

0 commit comments

Comments
 (0)