fix(#4768): degrade GET /api/crons gracefully when the cron module is absent
In split-container / minimal Docker deployments the WebUI image may not ship the
agent's cron package on its import path, so the Tasks tab's GET /api/crons did
'from cron.jobs import list_jobs' and 500'd with ModuleNotFoundError — breaking the
whole tab. Now the import is guarded and returns {jobs: [], cron_unavailable: true}
instead, so the Tasks tab loads (empty) instead of erroring. Happy path unchanged.
Co-authored-by: reporter
This commit is contained in:
parent
d2a64d7fb9
commit
34694a1f02
2 changed files with 56 additions and 1 deletions
|
|
@ -10188,7 +10188,13 @@ def handle_get(handler, parsed) -> bool:
|
|||
# os.environ (process-global) at call time. Wrap in cron_profile_context
|
||||
# so the TLS-active profile's jobs.json is read, not the process default.
|
||||
if parsed.path == "/api/crons":
|
||||
from cron.jobs import list_jobs
|
||||
# #4768: in split-container / minimal Docker deployments the WebUI image may
|
||||
# not ship the agent's `cron` package on its import path. Degrade gracefully
|
||||
# (empty list + cron_unavailable flag) instead of 500ing the whole Task tab.
|
||||
try:
|
||||
from cron.jobs import list_jobs
|
||||
except ModuleNotFoundError:
|
||||
return j(handler, {"jobs": [], "cron_unavailable": True})
|
||||
from api.profiles import cron_profile_context
|
||||
|
||||
with cron_profile_context():
|
||||
|
|
|
|||
49
tests/test_issue4768_cron_module_missing.py
Normal file
49
tests/test_issue4768_cron_module_missing.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""Regression test for #4768.
|
||||
|
||||
In split-container / minimal Docker deployments the WebUI image may not ship the
|
||||
agent's ``cron`` package on its import path. Before the fix, ``GET /api/crons``
|
||||
(the Tasks tab's initial load) did ``from cron.jobs import list_jobs`` with no
|
||||
guard, so a ``ModuleNotFoundError`` bubbled up as a 500 and broke the whole tab.
|
||||
|
||||
The fix degrades gracefully: an empty job list with a ``cron_unavailable`` flag.
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parent.parent
|
||||
ROUTES = (REPO / "api" / "routes.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _api_crons_branch() -> str:
|
||||
"""Return the source of the `if parsed.path == "/api/crons":` GET branch."""
|
||||
marker = ROUTES.index('if parsed.path == "/api/crons":')
|
||||
# Stop at the next sibling branch so we only inspect this handler.
|
||||
nxt = ROUTES.index('if parsed.path == "/api/crons/output":', marker)
|
||||
return ROUTES[marker:nxt]
|
||||
|
||||
|
||||
def test_api_crons_guards_missing_cron_module():
|
||||
"""The /api/crons GET branch must wrap the cron.jobs import in a guard that
|
||||
returns a graceful payload instead of letting ModuleNotFoundError 500."""
|
||||
branch = _api_crons_branch()
|
||||
assert "from cron.jobs import list_jobs" in branch
|
||||
assert "except ModuleNotFoundError" in branch, (
|
||||
"GET /api/crons must catch ModuleNotFoundError so the Tasks tab does not "
|
||||
"500 when the cron package is absent (#4768)."
|
||||
)
|
||||
# The except path returns an empty list + an unavailable flag (not a re-raise).
|
||||
assert "cron_unavailable" in branch
|
||||
# The try guards the import specifically (not some unrelated block).
|
||||
try_idx = branch.index("try:")
|
||||
import_idx = branch.index("from cron.jobs import list_jobs")
|
||||
except_idx = branch.index("except ModuleNotFoundError")
|
||||
assert try_idx < import_idx < except_idx
|
||||
|
||||
|
||||
def test_api_crons_still_lists_jobs_on_happy_path():
|
||||
"""The guard must not remove the normal behavior: when cron imports fine, the
|
||||
branch still calls list_jobs(include_disabled=True) under cron_profile_context."""
|
||||
branch = _api_crons_branch()
|
||||
assert "list_jobs(include_disabled=True)" in branch
|
||||
assert "cron_profile_context()" in branch
|
||||
assert "_cron_jobs_for_api(" in branch
|
||||
Loading…
Reference in a new issue