Skip to content

Commit 87d5e32

Browse files
authored
Merge pull request #3460 from lfelipediniz/feat/add_health_endpoint_to_Dash_framework
feat: Add /health endpoint for server monitoring
2 parents 74a30a9 + ef0f4ff commit 87d5e32

File tree

3 files changed

+88
-0
lines changed

3 files changed

+88
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1212
- [#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).
1313
- [#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.
1414
- [#3445](https://github.com/plotly/dash/pull/3445) Added API to reverse direction of slider component.
15+
- [#3460](https://github.com/plotly/dash/pull/3460) Add `/health` endpoint for server monitoring and health checks.
1516
- [#3465](https://github.com/plotly/dash/pull/3465) Plotly cloud integrations, add devtool API, placeholder plotly cloud CLI & publish button, `dash[cloud]` extra dependencies.
1617

1718
## Fixed

dash/dash.py

Lines changed: 15 additions & 0 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:
@@ -987,6 +995,13 @@ def serve_reload_hash(self):
987995
}
988996
)
989997

998+
def serve_health(self):
999+
"""
1000+
Health check endpoint for monitoring Dash server status.
1001+
Returns a simple "OK" response with HTTP 200 status.
1002+
"""
1003+
return flask.Response("OK", status=200, mimetype="text/plain")
1004+
9901005
def get_dist(self, libraries: Sequence[str]) -> list:
9911006
dists = []
9921007
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)