Skip to content

Commit f98acd4

Browse files
sirosenRadosław Kozicki
and
Radosław Kozicki
authored
Fix mtime header parsing (#567)
* Use calendar.timegm instead of time.mktime for UTC timestamps time.mktime() converts a time tuple in local time to seconds since the Epoch, as stated in the docs: > Convert a time tuple in local time to seconds since the Epoch. > Note that mktime(gmtime(0)) will not generally return zero for most > time zones; instead the returned value will either be equal to that > of the timezone or altzone attributes on the time module. calendar.timegm() is guaranteed to produce UTC timestamp: > Unrelated but handy function to calculate Unix timestamp from GMT. * Add a regression test for localtime handling * Add changelog entry for #565 --------- Co-authored-by: Radosław Kozicki <[email protected]>
1 parent bc2ce79 commit f98acd4

File tree

3 files changed

+43
-4
lines changed

3 files changed

+43
-4
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Unreleased
1313
- Update vendored schemas: bitbucket-pipelines, circle-ci, compose-spec, dependabot,
1414
github-workflows, gitlab-ci, mergify, renovate, woodpecker-ci (2025-05-11)
1515
- Fix: support ``click==8.2.0``
16+
- Fix a bug in ``Last-Modified`` header parsing which used local time and could
17+
result in improper caching. Thanks :user:`fenuks`! (:pr:`565`)
1618

1719
0.33.0
1820
------

src/check_jsonschema/cachedownloader.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import calendar
34
import contextlib
45
import hashlib
56
import io
@@ -43,7 +44,7 @@ def _resolve_cache_dir(dirname: str) -> str | None:
4344

4445
def _lastmod_from_response(response: requests.Response) -> float:
4546
try:
46-
return time.mktime(
47+
return calendar.timegm(
4748
time.strptime(response.headers["last-modified"], _LASTMOD_FMT)
4849
)
4950
# OverflowError: time outside of platform-specific bounds

tests/unit/test_cachedownloader.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@
1111
CacheDownloader,
1212
FailedDownloadError,
1313
_cache_hit,
14+
_lastmod_from_response,
1415
url_to_cache_filename,
1516
)
1617

1718
DEFAULT_RESPONSE_URL = "https://example.com/schema1.json"
19+
DEFAULT_LASTMOD = "Sun, 01 Jan 2000 00:00:01 GMT"
1820

1921

2022
def add_default_response():
2123
responses.add(
2224
"GET",
2325
DEFAULT_RESPONSE_URL,
24-
headers={"Last-Modified": "Sun, 01 Jan 2000 00:00:01 GMT"},
26+
headers={"Last-Modified": DEFAULT_LASTMOD},
2527
json={},
2628
match_querystring=None,
2729
)
@@ -274,10 +276,10 @@ def test_cachedownloader_handles_bad_lastmod_header(
274276
elif failure_mode == "time_overflow":
275277
add_default_response()
276278

277-
def fake_mktime(*args):
279+
def fake_timegm(*args):
278280
raise OverflowError("uh-oh")
279281

280-
monkeypatch.setattr("time.mktime", fake_mktime)
282+
monkeypatch.setattr("calendar.timegm", fake_timegm)
281283
else:
282284
raise NotImplementedError
283285

@@ -341,3 +343,37 @@ def dummy_validate_bytes(data):
341343
assert fp.read() == b"{}"
342344
# assert that the validator was not run
343345
assert validator_ran is False
346+
347+
348+
def test_lastmod_from_header_uses_gmtime(request, monkeypatch, default_response):
349+
"""
350+
Regression test for https://github.com/python-jsonschema/check-jsonschema/pull/565
351+
352+
The time was converted in local time, when UTC/GMT was desired.
353+
"""
354+
355+
def final_tzset():
356+
time.tzset()
357+
358+
request.addfinalizer(final_tzset)
359+
360+
response = requests.get(DEFAULT_RESPONSE_URL, stream=True)
361+
362+
with monkeypatch.context() as m:
363+
m.setenv("TZ", "GMT0")
364+
time.tzset()
365+
gmt_parsed_time = _lastmod_from_response(response)
366+
367+
with monkeypatch.context() as m:
368+
m.setenv("TZ", "EST5")
369+
time.tzset()
370+
est_parsed_time = _lastmod_from_response(response)
371+
372+
with monkeypatch.context() as m:
373+
m.setenv("TZ", "UTC0")
374+
time.tzset()
375+
utc_parsed_time = _lastmod_from_response(response)
376+
377+
# assert that they all match
378+
assert gmt_parsed_time == utc_parsed_time
379+
assert gmt_parsed_time == est_parsed_time

0 commit comments

Comments
 (0)