RSS - RSS pubDate is wrong on any non-UTC container: local time is relabelled as UTC - #4309 (#4314)

This commit is contained in:
dgtlmoon
2026-08-20 15:49:29 +02:00
committed by GitHub
parent 01cf56c8fb
commit 0956318000
3 changed files with 60 additions and 2 deletions
+3 -2
View File
@@ -150,7 +150,8 @@ def populate_feed_entry(fe, watch, content, guid, timestamp, link=None, title_su
fe.guid(guid, permalink=False)
# Set pubDate using the timestamp of this specific change
dt = datetime.datetime.fromtimestamp(int(timestamp))
dt = dt.replace(tzinfo=pytz.UTC)
# Note: tz= must be passed to fromtimestamp(), otherwise we get a naive datetime in the
# container's local timezone and relabelling it as UTC shifts every pubDate by the local offset
dt = datetime.datetime.fromtimestamp(int(timestamp), tz=pytz.UTC)
fe.pubDate(dt)
+4
View File
@@ -91,6 +91,10 @@ export HIDE_REFERER=True
REMOVE_REQUESTS_OLD_SCREENSHOTS=false pytest -vv -s --maxfail=1 tests/test_notification.py tests/test_access_control.py
# Re #4309 - RSS pubDate/timestamps must be correct on containers that don't run UTC
# (Europe/Athens is UTC+2/+3, so any naive local->UTC relabelling shows up as a shifted date)
TZ=Europe/Athens pytest -vv -s --maxfail=1 tests/test_rss.py
# Re-run a few tests that will trigger brotli based storage
# And again with brotli+screenshot attachment
SNAPSHOT_BROTLI_COMPRESSION_THRESHOLD=5 REMOVE_REQUESTS_OLD_SCREENSHOTS=false pytest -vv -s --maxfail=1 --dist=load tests/test_backend.py tests/test_rss.py tests/test_unique_lines.py tests/test_notification.py tests/test_access_control.py
+53
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
import email.utils
import os
import time
import pytest
from flask import url_for
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks, extract_rss_token_from_UI, \
extract_UUID_from_client, delete_all_watches
@@ -354,3 +356,54 @@ def test_rss_single_watch_feed(client, live_server, measure_memory_usage, datast
assert (">3<" in descriptions[2] or "Version 3" in descriptions[2]) and "content" in descriptions[2], \
f"Third item should show Version 3, but got: {descriptions[2][:500]}"
# #4309 - pubDate must be the real UTC time of the change, whatever the server's local timezone is.
# The GUID carries the snapshot timestamp, so the two must agree to the second.
for item in items:
guid_timestamp = int(item.findtext('guid').rsplit('/', 1)[1])
pub_date = email.utils.parsedate_to_datetime(item.findtext('pubDate'))
assert pub_date.timestamp() == guid_timestamp, \
f"pubDate {item.findtext('pubDate')} does not match snapshot timestamp {guid_timestamp}"
@pytest.mark.skipif(not hasattr(time, 'tzset'), reason="Changing TZ at runtime needs a POSIX platform")
@pytest.mark.parametrize("tz_name", ['UTC', 'Europe/Athens', 'America/New_York', 'Australia/Sydney'])
def test_rss_pubdate_is_utc_regardless_of_local_timezone(tz_name):
"""#4309 - datetime.fromtimestamp() without tz= returns local wall-clock time, and relabelling
that as UTC shifts every pubDate by the local offset (items appear in the future on TZ=Europe/Athens).
No live server needed - this drives populate_feed_entry() directly under several timezones."""
import xml.etree.ElementTree as ET
from feedgen.feed import FeedGenerator
from ..blueprint.rss._util import populate_feed_entry
timestamp = 1700000000 # Tue, 14 Nov 2023 22:13:20 UTC
original_tz = os.environ.get('TZ')
try:
os.environ['TZ'] = tz_name
time.tzset()
fg = FeedGenerator()
fg.title('test')
fg.link(href='https://example.com', rel='self')
fg.description('test')
fe = fg.add_entry()
populate_feed_entry(fe=fe,
watch={'uuid': 'fake-uuid', 'url': 'https://example.com'},
content='some content',
guid=f'fake-uuid/{timestamp}',
timestamp=timestamp)
pub_date = ET.fromstring(fg.rss_str()).findtext('.//item/pubDate')
finally:
if original_tz is None:
os.environ.pop('TZ', None)
else:
os.environ['TZ'] = original_tz
time.tzset()
# parsedate_to_datetime() honours the offset in the header, so this compares real instants
assert email.utils.parsedate_to_datetime(pub_date).timestamp() == timestamp, \
f"With TZ={tz_name} the feed said {pub_date}, expected the instant {timestamp}"