Skip to content

Commit aa4cf29

Browse files
[PR #13170/2b906869 backport][3.14] Fix StreamResponse.last_modified rounding inconsistency between datetime and timestamp (#13192)
**This is a backport of PR #13170 as merged into master (2b90686).** Co-authored-by: agu2347 <94227848+agu2347@users.noreply.github.com>
1 parent 71b57b4 commit aa4cf29

3 files changed

Lines changed: 26 additions & 0 deletions

File tree

‎CHANGES/5303.bugfix.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fixed :py:attr:`~aiohttp.web.StreamResponse.last_modified` rounding a
2+
:class:`datetime.datetime` with a fractional second down while rounding an
3+
equivalent :class:`float` timestamp up, so the two produced different
4+
``Last-Modified`` headers for the same instant. Both now round up to the
5+
next whole second, consistent with how :py:class:`~aiohttp.web.FileResponse`
6+
relies on this rounding to avoid falsely reporting a resource as
7+
unmodified.

‎aiohttp/web_response.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ def last_modified(
371371
"%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))
372372
)
373373
elif isinstance(value, datetime.datetime):
374+
if value.microsecond:
375+
value = value.replace(microsecond=0) + datetime.timedelta(seconds=1)
374376
self._headers[hdrs.LAST_MODIFIED] = time.strftime(
375377
"%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple()
376378
)

‎tests/test_web_response.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,23 @@ def test_last_modified_datetime() -> None:
339339
assert resp.last_modified == dt
340340

341341

342+
def test_last_modified_datetime_and_timestamp_round_consistently() -> None:
343+
dt = datetime.datetime(2020, 12, 2, 9, 51, 2, 500000, datetime.timezone.utc)
344+
345+
resp_dt = web.StreamResponse()
346+
resp_dt.last_modified = dt
347+
348+
resp_ts = web.StreamResponse()
349+
resp_ts.last_modified = dt.timestamp()
350+
351+
assert resp_dt.headers["Last-Modified"] == resp_ts.headers["Last-Modified"]
352+
# Both should round up to the next whole second, so that Last-Modified
353+
# is never earlier than the file's true st_mtime, invalidating the caching.
354+
assert resp_dt.last_modified == datetime.datetime(
355+
2020, 12, 2, 9, 51, 3, 0, datetime.timezone.utc
356+
)
357+
358+
342359
def test_last_modified_reset() -> None:
343360
resp = StreamResponse()
344361

0 commit comments

Comments
 (0)