Skip to content

fix(timezone): treat zoneinfo's ValueError as an unknown timezone - #1026

Open
lihongyuan99 wants to merge 1 commit into
TencentCloud:developfrom
lihongyuan99:fix/zoneinfo-valueerror-not-a-timezone
Open

lihongyuan99 wants to merge 1 commit into
TencentCloud:developfrom
lihongyuan99:fix/zoneinfo-valueerror-not-a-timezone

Conversation

@lihongyuan99

@lihongyuan99 lihongyuan99 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Several timezone helpers implement a fallback — "unknown zone → UTC" or "unknown zone → 400" — but only catch ZoneInfoNotFoundError. That is a KeyError subclass; zoneinfo raises a plain ValueError for keys it refuses to resolve at all (absolute paths, ./ or .. segments — see zoneinfo/_tzpath.py::_validate_tzfile_path), so those values escape the fallback:

  • src/octop/api/routers/preferences.py:198 — PATCH /api/preferences {"timezone": "/etc/localtime"} dies with ValueError: ZoneInfo keys may not be absolute paths instead of the OctopError(SLASH_BAD_ARGS, "timezone must be a valid IANA timezone") the very same block raises for BAD_ZONE.
  • src/octop/infra/gateway/slash/formatting.py:14 — config.default_timezone is never validated (config.py:607 only does str(merged.get(...) or "Asia/Shanghai")), so a path-shaped value raises out of the slash /history handler (composite.py:167 → format_unix_datetime).
  • src/octop/api/common/usage_xlsx.py:30 — same, breaks GET /api/usage/export.xlsx (api/routers/usage.py:63 reads config.default_timezone).
  • src/octop/infra/proactive/scheduler.py:129,393 — same, raises inside the proactive-care loop.

/etc/localtime is the canonical tzif path on Linux, and the config field is free-form text, so an operator who copies the host path into default_timezone gets a crash instead of the fallback the code already implements.

Fix: widen the four catches. Behavior for valid zones and for genuinely-missing zone names is unchanged — only the keys that previously crashed now take the fallback that is already written there.

infra/db/repos/usage.py:66-70 has the identical narrow catch, and it is what GET /api/usage/summary hits through api/routers/usage.py:83 — so that route keeps crashing on a path-shaped zone. Left alone on purpose because #986 is rewriting that file right now; happy to follow up once it lands.

Heads-up on overlap: #604 also edits patch_preferences, but its last hunk ends at pre-change line 147 and this one is at 156, so the two diffs do not touch the same lines.

Target branch

  • Base is develop (feature / fix — default)
  • Base is main (release/* or hotfix/* only)

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactor / chore
  • Release / hotfix

Test plan

Red first on the rebased base: with this branch's tests and develop@4667bd7b's four source
files (fix temporarily reverted), the same exception escapes each fallback:

uv run pytest -q tests/integration/test_preferences_api.py tests/unit/gateway/test_slash_history.py \
  tests/unit/proactive/test_scheduler.py tests/unit/api/test_usage_xlsx.py
ValueError: ZoneInfo keys may not be absolute paths, got: /etc/localtime
FAILED tests/integration/test_preferences_api.py::test_preferences_timezone_rejects_invalid_value
FAILED tests/unit/gateway/test_slash_history.py::test_format_unix_datetime_uses_server_timezone
FAILED tests/unit/proactive/test_scheduler.py::test_is_in_active_hours_with_path_shaped_timezone_falls_back_to_utc
FAILED tests/unit/api/test_usage_xlsx.py::test_build_usage_xlsx_falls_back_for_unresolvable_timezone
4 failed, 30 passed in 11.13s

Green with the fix, plus the neighbouring file:

uv run pytest -q tests/integration/test_preferences_api.py tests/unit/gateway/test_slash_history.py \
  tests/unit/proactive/test_scheduler.py tests/unit/api/test_usage_xlsx.py tests/unit/gateway/test_slash_compact.py
42 passed in 4.05s

Backend gates, re-run after rebasing onto develop@4667bd7b (equivalent to the CI steps;
make all itself was not run — this clone has no dashboard node_modules, so the build step
it chains cannot run here):

uv run ruff check src tests          -> All checks passed!
uv run ruff format --check src tests -> 1081 files already formatted
uv run mypy src/octop                -> Success: no issues found in 530 source files

Full suite, same command, base commit vs this branch:

develop  4667bd7b  ->  3847 passed, 17 skipped, 0 failed
this branch       ->  3849 passed, 17 skipped, 0 failed   (+2 = the two new cases)

tests/live was not run; no model API key or PostgreSQL was needed.

Checklist

  • Updated CHANGELOG.md (if user-facing)
  • README / docs updated (if needed)

Rebased onto develop@ded7addd on 2026-09-30. 1.0.2b4 and 1.0.2b5 both shipped since the previous base, and each release commit folded the whole ## [Unreleased] list into a version block, which emptied that section on develop — it is this branch's only conflict. The resolution keeps develop's CHANGELOG.md verbatim and re-inserts this PR's single bullet under a fresh ### 修复; the rest of the patch is unchanged.

Everything below was re-measured on develop@ded7addd. Backend gates were run by hand, as the equivalents of the CI steps (make lint / make typecheck / make test); make all was not used because this clone has no dashboard/node_modules.

  • Red, with this PR's fix reverted and only its tests: uv run pytest -q tests/integration/test_preferences_api.py tests/unit/gateway/test_slash_history.py tests/unit/proactive/test_scheduler.py tests/unit/api/test_usage_xlsx.py -> 4 failed, 30 passed in 5.81s
  • Green as committed: the same command -> 34 passed in 6.18s
  • uv run ruff check src tests -> All checks passed!; uv run ruff format --check src tests -> 1112 files already formatted
  • uv run mypy src/octop -> Success: no issues found in 543 source files
  • Full suite uv run pytest -q -m "not live" -n 6 -> 3955 passed, 17 skipped, 1 failed in 214.82s, against 3953 passed, 17 skipped, 1 failed in 225.23s measured on the unmodified base commit in the same virtualenv. The single failure in both runs is tests/unit/test_plugin_manager.py::test_install_plugin_importing_legacy_harness_agent: legacy_imports.ensure_legacy_harness_agent_alias() deliberately leaves a real harness_agent distribution alone, and this local venv has an out-of-lock orcakit-harness-agent installed, so the alias assertion cannot hold here. It is pre-existing on the base, not a case this PR adds, and it is green in CI, which installs strictly from uv.lock (the Python 3.12 quality job is success on both this branch's pre- and post-rebase heads).
  • tests/live was not run; no model API key, PostgreSQL or Docker was needed.

Rebased onto develop@83b2ae2e7 (2026-10-03).

Four commits landed on develop since this branch's previous base (a02e3e18e): LDAP directory login (#1123), propagating exhausted model retries as failures (#1012), and two dashboard fixes (#1429, #1241) — each appended a bullet to ## [Unreleased], so CHANGELOG.md is this PR's only conflict. The resolution keeps develop's CHANGELOG.md verbatim and re-inserts this PR's single line into the existing ### 修复 list: git diff origin/develop 470f1a2ac -- CHANGELOG.md is +1/-0, and git diff 1f54c7ffc0 470f1a2aca over every non-CHANGELOG file of the PR is empty, i.e. the code you already reviewed is byte-identical.

Re-measured on the new base with the same commands and the same virtualenv as the Test plan above:

  • ruff check src tests → All checks passed!
  • ruff format --check src tests → 1134 files already formatted
  • mypy src/octop → Success: no issues found in 551 source files
  • this PR's own tests: pytest -q tests/integration/test_preferences_api.py tests/unit/api/test_usage_xlsx.py tests/unit/gateway/test_slash_history.py tests/unit/proactive/test_scheduler.py → 34 passed, 2 warnings in 5.65s
  • full suite pytest -q -m "not live" -n 3 → 1 failed, 4134 passed, 17 skipped, 13 warnings in 432.89s (0:07:12) (three branches measured concurrently, so the wall time is inflated), against 4132 passed, 17 skipped, 1 failed in 218.55s measured on the unmodified base 83b2ae2e7 in this same virtualenv: +2 cases against the base, which are exactly this PR's. The single failure in both runs is the pre-existing local artifact tests/unit/test_plugin_manager.py::test_install_plugin_importing_legacy_harness_agent — this venv has an out-of-lock orcakit-harness-agent distribution installed, and that test deliberately leaves a real harness_agent package alone, so its alias assertion cannot hold here. It is red on the untouched base too and green in CI, which installs strictly from uv.lock.

2026-10-06 — CHANGELOG.md re-aligned with develop. A rebase of this branch had re-introduced other PRs' release-note bullets into the released ## [1.0.2b6] block, so this PR's diff carried those foreign lines instead of only its own. CHANGELOG.md is now develop's file verbatim plus this PR's single bullet under ## [Unreleased] → ### 修复:

  • git diff --numstat 75c274aef a1868e0ca -- CHANGELOG.md → 1\t0\tCHANGELOG.md
  • git diff --name-only ceb0e4e0f a1868e0ca → CHANGELOG.md only, so every code and test file is byte-identical to the head that Python 3.12 and Windows / Python 3.12 both reported success on;
    the commands and counts under Test plan therefore still describe this diff.
  • CHANGELOG.md at 75c274aef is identical to today's develop tip, so this does not need a fresh rebase.

Rebased onto develop@97d6e7ff4 (2026-10-12); head a1868e0ca -> 3e0a2ef1b.

CHANGELOG.md is again this branch's only conflict, and the resolution keeps develop's file
verbatim and re-inserts only this PR's own bullet under its matching ### subsection — checked
mechanically: the diff against the new base is 1 added / 0 deleted in CHANGELOG.md
and byte-identical (hunk headers aside) for 8 code/test file(s) before and after the
rebase. Gates re-run by hand on the new head (make all still does not apply — no
dashboard/node_modules in this clone; the venv is created per worktree so
/Users/macos/Documents/Qoder/octop-pr-mining/repos/wt-r-1026/src/octop/__init__.py is this branch's own code):

  • targeted tests for this branch: 34 passed, 8 warnings in 9.27s
  • full suite: 4629 passed, 26 skipped, 17 warnings in 410.59s (0:06:50), against develop@97d6e7ff4 measured in the same venv: 4627 passed, 26 skipped, 0 failed
  • All checks passed!; 1180 files already formatted; Success: no issues found in 570 source files

Rebased onto develop@87a8d52c2 (2026-10-14); head 3e0a2ef1b -> 8d9a06af9.

CHANGELOG.md is again the only conflict. develop's file is kept verbatim and this PR's own
bullet is re-inserted into the current ## [Unreleased] list, which is now flat (no ###
subsections), so the diff against the new base is 1 added / 0 deleted there and the
8 code/test file(s) are byte-identical before and after (hunk headers aside) — checked
mechanically, not by eye. Gates re-run by hand on the new head (make all still does not apply:
no dashboard/node_modules in this clone; the venv belongs to this worktree, so
/Users/macos/Documents/Qoder/octop-pr-mining/repos/wt-r-1026/src/octop/__init__.py is this branch's own code):

  • RC ruff=0 format=0 mypy=0 targeted=0 full=0
  • targeted tests for this branch: 34 passed, 8 warnings in 11.11s
  • full suite: 4681 passed, 26 skipped, 41 warnings in 242.98s (0:04:02), against develop@87a8d52c2 measured in the same environment today: 4679 passed, 26 skipped, 0 failed
  • All checks passed!; 1186 files already formatted; Success: no issues found in 571 source files

Rebased onto develop@99e479a6b (2026-10-16); head 255fca5bf -> 597dcbdcb.

CHANGELOG.md was again the only conflict: develop's file is kept verbatim and this PR's own bullet is re-inserted under its matching ### subsection of the current ## [Unreleased] block, so the diff against the new base is 1 added / 0 deleted there and the 8 code/test file(s) are byte-identical before and after the rebase (checked mechanically on the patch hash, not by eye). Gates re-run by hand on the new head (make all still does not apply — no dashboard/node_modules in this clone; the venv belongs to this worktree, so the checkout under test is this branch's own code):

  • RC ruff=0 format=0 mypy=0 targeted=0 full=0
  • targeted tests for this branch: 34 passed, 8 warnings in 22.82s
  • full suite: 4782 passed, 32 skipped, 41 warnings in 448.26s (0:07:28), against develop@99e479a6b measured today in its own per-worktree venv on the same machine: 4780 passed, 32 skipped, 0 failed in 495.68s
  • All checks passed!; 1204 files already formatted; Success: no issues found in 581 source files

@lihongyuan99
lihongyuan99 force-pushed the fix/zoneinfo-valueerror-not-a-timezone branch 5 times, most recently from cc99fb0 to 594d338 Compare September 29, 2026 23:23
@lihongyuan99
lihongyuan99 force-pushed the fix/zoneinfo-valueerror-not-a-timezone branch 5 times, most recently from ceb0e4e to a1868e0 Compare October 5, 2026 23:14
@lihongyuan99
lihongyuan99 force-pushed the fix/zoneinfo-valueerror-not-a-timezone branch 3 times, most recently from 8d9a06a to 255fca5 Compare October 14, 2026 23:18
The timezone helpers that fall back to UTC / to a 400 only catch
ZoneInfoNotFoundError, which is a KeyError subclass. zoneinfo raises
plain ValueError for keys it refuses to resolve at all -- absolute
paths, "./" or ".." segments -- so a value like /etc/localtime or
./Asia/Shanghai escaped instead:

  - PATCH /api/preferences returned 500 rather than the "timezone must
    be a valid IANA timezone" the same block raises for BAD_ZONE
  - /status (slash formatting), the usage .xlsx export and the
    proactive-care scheduler lost their documented UTC fallback

Widen each of those four catches. The same narrow catch also sits in
infra/db/repos/usage.py:_zoneinfo, left alone because TencentCloud#986 is changing
that file.
@lihongyuan99
lihongyuan99 force-pushed the fix/zoneinfo-valueerror-not-a-timezone branch from 255fca5 to 597dcbd Compare October 15, 2026 23:15

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant