Skip to content

Commit 6d36759

Browse files
authored
Harden Flask health endpoint registration (#159)
1 parent 81b0bd5 commit 6d36759

2 files changed

Lines changed: 88 additions & 4 deletions

File tree

‎src/quant_platform_kit/common/health.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,26 @@ def read(self) -> Heartbeat | None:
180180

181181
def _register_flask(self) -> None:
182182
from flask import jsonify
183-
monitor = self
184183

185-
@self._app.route("/health", methods=["GET"])
186-
@self._app.route("/healthz", methods=["GET"])
187-
def health():
184+
def qpk_health():
188185
return jsonify({"status": "ok", "timestamp": datetime.now(timezone.utc).isoformat()})
189186

187+
existing_rules = {getattr(rule, "rule", "") for rule in self._app.url_map.iter_rules()}
188+
if "/health" not in existing_rules:
189+
self._app.add_url_rule(
190+
"/health",
191+
endpoint="qpk_health",
192+
view_func=qpk_health,
193+
methods=["GET"],
194+
)
195+
if "/healthz" not in existing_rules:
196+
self._app.add_url_rule(
197+
"/healthz",
198+
endpoint="qpk_healthz",
199+
view_func=qpk_health,
200+
methods=["GET"],
201+
)
202+
190203
def _start_http_server(self) -> None:
191204
import http.server
192205
port = self._http_port

‎tests/test_common_health.py‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
import types
5+
from collections.abc import Callable
6+
from dataclasses import dataclass
7+
from typing import Any
8+
9+
from quant_platform_kit.common.health import register_health_endpoint
10+
11+
12+
@dataclass
13+
class _FakeRule:
14+
rule: str
15+
16+
17+
class _FakeUrlMap:
18+
def __init__(self, app: _FakeFlaskApp):
19+
self._app = app
20+
21+
def iter_rules(self):
22+
return iter(self._app.rules)
23+
24+
25+
class _FakeFlaskApp:
26+
def __init__(self):
27+
self.rules: list[_FakeRule] = []
28+
self.view_functions: dict[str, Callable[..., Any]] = {}
29+
self.url_map = _FakeUrlMap(self)
30+
31+
def add_url_rule(
32+
self,
33+
rule: str,
34+
endpoint: str,
35+
view_func: Callable[..., Any],
36+
methods: list[str],
37+
) -> None:
38+
if endpoint in self.view_functions:
39+
raise AssertionError(
40+
f"View function mapping is overwriting an existing endpoint function: {endpoint}"
41+
)
42+
self.rules.append(_FakeRule(rule))
43+
self.view_functions[endpoint] = view_func
44+
45+
46+
def _install_fake_flask(monkeypatch):
47+
monkeypatch.setitem(sys.modules, "flask", types.SimpleNamespace(jsonify=lambda payload: payload))
48+
49+
50+
def test_register_health_endpoint_uses_non_colliding_endpoint_names(monkeypatch):
51+
_install_fake_flask(monkeypatch)
52+
app = _FakeFlaskApp()
53+
register_health_endpoint(app)
54+
55+
app.add_url_rule("/", endpoint="health", view_func=lambda: "service-info", methods=["GET"])
56+
57+
assert {rule.rule for rule in app.rules} == {"/health", "/healthz", "/"}
58+
assert {"qpk_health", "qpk_healthz", "health"} <= set(app.view_functions)
59+
60+
61+
def test_register_health_endpoint_preserves_existing_health_route(monkeypatch):
62+
_install_fake_flask(monkeypatch)
63+
app = _FakeFlaskApp()
64+
app.add_url_rule("/health", endpoint="platform_health", view_func=lambda: "platform-ok", methods=["GET"])
65+
66+
register_health_endpoint(app)
67+
68+
assert [rule.rule for rule in app.rules] == ["/health", "/healthz"]
69+
assert "platform_health" in app.view_functions
70+
assert "qpk_health" not in app.view_functions
71+
assert "qpk_healthz" in app.view_functions

0 commit comments

Comments
 (0)