parse_time_range mishandles DST spring-forward gaps that aren't exactly one hour (crashes on >1h gaps, wrong instant on <1h gaps)
- Dominant language
- TypeScript
- Stars
- 156k
- Forks
- 24.6k
- Avg merge
- 22h 9m
- Merged PRs (30d)
- 610
Description
### Self Checks
- [x] I have searched for existing issues, including closed ones.
- [x] I confirm that I am using English to submit this report.
### Dify version
main (`cb41fb3e76`)
### Cloud or Self Hosted
Self Hosted (Source)
### Steps to reproduce
`libs/datetime_utils.py::parse_time_range` resolves a non-existent (DST spring-forward) wall-clock time by adding a hardcoded one hour:
```python
except pytz.NonExistentTimeError:
dt += datetime.timedelta(hours=1)
return tz.localize(dt, is_dst=None).astimezone(utc)
```
This assumes every spring-forward gap is exactly one hour. Real tzdata (pytz 2025.2 in the repo venv) has gaps of other sizes, giving two failure modes:
**1. Gaps longer than one hour → uncaught crash.** Antarctica/Troll springs forward 2 hours (2024-03-31 01:00 → 03:00), so 01:30 is non-existent. Adding one hour lands at 02:30, still inside the gap, so the retry `tz.localize(..., is_dst=None)` raises `NonExistentTimeError` again — this time uncaught:
```python
>>> from libs.datetime_utils import parse_time_range
>>> parse_time_range('2024-03-31 01:30', None, 'Antarctica/Troll')
pytz.exceptions.NonExistentTimeError: 2024-03-31 02:30:00
```
The docstring says it raises `ValueError` only and "handles ... non-existent times gracefully"; instead a raw pytz exception escapes into the analytics/statistics API path. `Pacific/Apia` (2011-12-30, a fully skipped day) crashes the same way.
**2. Gaps shorter than one hour → wrong UTC instant.** Australia/Lord_Howe springs forward only 30 minutes (2024-10-06 02:00 +10:30 → 02:30 +11:00), so 02:15 is non-existent. The correct instant (zoneinfo fold=0) is 02:15 +10:30 = 2024-10-05 **15:45** UTC, but the code returns **16:15** UTC:
```python
>>> parse_time_range('2024-10-06 02:15', None, 'Australia/Lord_Howe')[0]
datetime.datetime(2024, 10, 5, 16, 15, tzinfo=) # expected 15:45
```
For a statistics start-bound this silently shifts the window by 30 minutes.
### Fix
Resolve the non-existent time with `tz.normalize(tz.localize(dt, is_dst=False))`, which shifts forward by exactly the gap size for any offset change and matches zoneinfo fold=0. Standard one-hour gaps are unchanged (America/New_York 02:30 → 07:30 UTC). PR with real-timezone tests follows.
Contributor guide
Research direction
Start in libs/datetime_utils.py at parse_time_range and reproduce the Antarctica/Troll and Australia/Lord_Howe examples from the issue. Add real-timezone coverage for gaps longer and shorter than one hour, then verify standard America/New_York behavior and that the documented ValueError contract is preserved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- Half a day
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100