Skip to content

Commit 4aa5769

Browse files
committed
[fix] Graceful fallback to default timezone for invalid inputs
Modified MonitoringApiViewMixin to handle UnknownTimeZoneError by falling back to the system default timezone. Added strict regression tests in device and monitoring modules to verify chart data availability on fallback.
1 parent 98afa9b commit 4aa5769

4 files changed

Lines changed: 76 additions & 6 deletions

File tree

openwisp_monitoring/device/tests/test_api.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ def test_get_device_metrics_empty(self):
640640
self.assertEqual(response.status_code, 200)
641641
self.assertEqual(response.data["charts"], [])
642642

643-
def test_get_device_metrics_400_bad_timezone(self):
643+
def test_get_device_metrics_bad_timezone_fallback(self):
644644
dd = self.create_test_data(no_resources=True)
645645
d = self.device_model.objects.get(pk=dd.pk)
646646
wrong_timezone_values = (
@@ -650,9 +650,10 @@ def test_get_device_metrics_400_bad_timezone(self):
650650
)
651651
for tz_value in wrong_timezone_values:
652652
url = "{0}&timezone={1}".format(self._url(d.pk, d.key), tz_value)
653-
r = self.client.get(url)
654-
self.assertEqual(r.status_code, 400)
655-
self.assertIn("Unkown Time Zone", r.data)
653+
response = self.client.get(url)
654+
self.assertEqual(response.status_code, 200)
655+
self.assertIn("charts", response.data)
656+
self.assertGreater(len(response.data["charts"]), 0)
656657

657658
def test_device_metrics_received_signal(self):
658659
d = self._create_device(organization=self._create_org())

openwisp_monitoring/monitoring/tests/test_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,32 @@ def test_group_by_time(self):
679679
response = self.client.get(path, {"time": "3w"})
680680
self.assertEqual(response.status_code, 400)
681681

682+
def test_legacy_timezone_fallback(self):
683+
"""
684+
Pass a legacy timezone, without crashing the API
685+
with a 500 error when OS zoneinfo is missing. fix for monitoring#728
686+
"""
687+
admin = self._create_admin()
688+
self.client.force_login(admin)
689+
path = reverse("monitoring_general:api_dashboard_timeseries")
690+
691+
# Test with a legacy timezone that isn't in modern tzdata
692+
with self.subTest("Test legacy timezone normalization (Asia/Calcutta)"):
693+
response = self.client.get(
694+
path, {"timezone": "Asia/Calcutta", "time": "7d"}
695+
)
696+
self.assertEqual(response.status_code, 200)
697+
self.assertIsInstance(response.data.get("charts"), list)
698+
699+
# Test with an invalid timezone to see if fallback works.
700+
with self.subTest("Test invalid timezone fallback (Antarctica/Banana)"):
701+
response = self.client.get(
702+
path, {"timezone": "Antarctica/Banana", "time": "7d"}
703+
)
704+
self.assertEqual(response.status_code, 200)
705+
self.assertIn("charts", response.data)
706+
self.assertGreater(len(response.data["charts"]), 0)
707+
682708
def test_organizations_list(self):
683709
path = reverse("monitoring_general:api_dashboard_timeseries")
684710
Organization.objects.all().delete()

openwisp_monitoring/utils.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
2-
from functools import wraps
2+
from functools import lru_cache, wraps
33
from time import sleep
4+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
45

6+
from django.conf import settings
57
from django.db import transaction
68

79
from .settings import MONITORING_TIMESERIES_RETRY_OPTIONS
@@ -33,3 +35,42 @@ def wrapper(*args, **kwargs):
3335
raise err
3436

3537
return wrapper
38+
39+
40+
@lru_cache(maxsize=128)
41+
def normalize_timezone(tz_name):
42+
"""
43+
Normalizes legacy timezones and verifies availability via zoneinfo.
44+
"""
45+
if not tz_name:
46+
return getattr(settings, "TIME_ZONE", "UTC")
47+
48+
# Mapping of legacy timezones
49+
legacy_map = {
50+
"Asia/Calcutta": "Asia/Kolkata",
51+
"Asia/Saigon": "Asia/Ho_Chi_Minh",
52+
"Asia/Katmandu": "Asia/Kathmandu",
53+
"US/Eastern": "America/New_York",
54+
"US/Pacific": "America/Los_Angeles",
55+
}
56+
57+
normalized_tz = legacy_map.get(tz_name, tz_name)
58+
59+
# Verify timezone existence in a cross-platform way.
60+
try:
61+
ZoneInfo(normalized_tz)
62+
if normalized_tz != tz_name:
63+
logger.info(
64+
f"Normalized deprecated timezone '{tz_name}' to '{normalized_tz}'"
65+
)
66+
return normalized_tz
67+
except ZoneInfoNotFoundError:
68+
pass
69+
70+
# Fallback if above is false
71+
fallback = getattr(settings, "TIME_ZONE", "UTC")
72+
logger.warning(
73+
f"Timezone '{normalized_tz}' (original: '{tz_name}') not found in OS zoneinfo. "
74+
f"Falling back to system TIME_ZONE '{fallback}'."
75+
)
76+
return fallback

openwisp_monitoring/views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from swapper import load_model
1515

1616
from .monitoring.exceptions import InvalidChartConfigException
17+
from .utils import normalize_timezone
1718

1819
logger = logging.getLogger(__name__)
1920

@@ -54,7 +55,8 @@ def get(self, request, *args, **kwargs):
5455
start_date = request.query_params.get("start", None)
5556
end_date = request.query_params.get("end", None)
5657
# try to read timezone
57-
timezone = request.query_params.get("timezone", settings.TIME_ZONE)
58+
raw_timezone = request.query_params.get("timezone", settings.TIME_ZONE)
59+
timezone = normalize_timezone(raw_timezone)
5860
try:
5961
tz(timezone)
6062
except UnknownTimeZoneError:

0 commit comments

Comments
 (0)