Skip to content

Commit 9ab096a

Browse files
vdusekclaude
andauthored
fix: treat naive datetime query params as UTC (#752)
## Summary `HttpClientBase._parse_params` called `.astimezone(UTC)` directly on incoming `datetime` values. Per Python's stdlib, a naive datetime is presumed to represent time in the system's local timezone, so on a non-UTC host the value was shifted by the host's offset and then serialized with a `Z` suffix — a wire format that *looks* like UTC but isn't. This silently corrupted timestamp filters such as `started_before`/`started_after` on `runs().list()` whenever a caller passed a naive datetime. The fix attaches `UTC` to the value when `tzinfo` is missing, so naive datetimes round-trip as UTC instead of being relocalized. Added a unit test that pins `TZ=Asia/Karachi` with `time.tzset()` to reproduce the bug deterministically on Unix CI (skipped on Windows, where `time.tzset` is unavailable). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5a32f25 commit 9ab096a

2 files changed

Lines changed: 18 additions & 1 deletion

File tree

src/apify_client/_http_clients/_base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ def _parse_params(params: dict[str, Any] | None) -> dict[str, Any] | None:
157157
elif isinstance(value, list):
158158
parsed_params[key] = ','.join(value)
159159
elif isinstance(value, datetime):
160-
utc_aware_dt = value.astimezone(UTC)
160+
# Treat a naive datetime as UTC; `.astimezone()` would otherwise assume the host's local tz.
161+
aware = value.replace(tzinfo=UTC) if value.tzinfo is None else value
162+
utc_aware_dt = aware.astimezone(UTC)
161163
iso_str = utc_aware_dt.isoformat(timespec='milliseconds')
162164
parsed_params[key] = iso_str.replace('+00:00', 'Z')
163165
elif value is not None:

tests/unit/test_http_clients.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,21 @@ def test_parse_params_datetime() -> None:
201201
assert result == {'created_at': '2024-01-15T10:30:45.123Z'}
202202

203203

204+
@pytest.mark.skipif(not hasattr(time, 'tzset'), reason='time.tzset is Unix-only')
205+
def test_parse_params_naive_datetime_treated_as_utc(monkeypatch: pytest.MonkeyPatch) -> None:
206+
"""Naive datetimes must be treated as UTC, not the host's local timezone."""
207+
monkeypatch.setenv('TZ', 'Asia/Karachi')
208+
time.tzset()
209+
try:
210+
dt = datetime(2024, 1, 15, 10, 30, 45, 123000) # noqa: DTZ001 -- intentionally naive
211+
result = HttpClient._parse_params({'created_at': dt})
212+
assert result == {'created_at': '2024-01-15T10:30:45.123Z'}
213+
finally:
214+
# Restore TZ before re-applying tzset so the test doesn't leak Karachi time to later tests.
215+
monkeypatch.undo()
216+
time.tzset()
217+
218+
204219
def test_parse_params_none_values_filtered() -> None:
205220
"""Test _parse_params filters out None values."""
206221
result = HttpClient._parse_params({'a': 1, 'b': None, 'c': 'value'})

0 commit comments

Comments
 (0)