Always assume UTC if no timezone in Retry-After
MODIFY
dev-requirements.txt
dev-requirements.txt
@@ -15,3 +15,5 @@ gcp-devrel-py-tools==0.0.15
# https://github.com/GoogleCloudPlatform/python-repo-tools/issues/23
pylint<2.0;python_version<="2.7"
+
+python-dateutil==2.8.1

MODIFY
src/urllib3/util/retry.py
src/urllib3/util/retry.py
@@ -269,6 +269,13 @@ class Retry(object):
retry_date_tuple = email.utils.parsedate_tz(retry_after)
if retry_date_tuple is None:
raise InvalidHeader("Invalid Retry-After header: %s" % retry_after)
+ if retry_date_tuple[9] is None: # Python 2
+ # Assume UTC if no timezone was specified
+ # On Python2.7, parsedate_tz returns None for a timezone offset
+ # instead of 0 if no timezone is given, where mktime_tz treats
+ # a None timezone offset as local time.
+ retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:]
+
retry_date = email.utils.mktime_tz(retry_date_tuple)
seconds = retry_date - time.time()

MODIFY
test/conftest.py
test/conftest.py
@@ -8,6 +8,8 @@ import pytest
import trustme
from tornado import web, ioloop
+from .tz_stub import stub_timezone_ctx
+
from dummyserver.handlers import TestingApp
from dummyserver.server import run_tornado_app
from dummyserver.server import HAS_IPV6
@@ -96,3 +98,12 @@ def ipv6_san_server(tmp_path_factory):
with run_server_in_thread("https", "::1", tmpdir, ca, server_cert) as cfg:
yield cfg
+
+
+@pytest.yield_fixture
+def stub_timezone(request):
+ """
+ A pytest fixture that runs the test with a stub timezone.
+ """
+ with stub_timezone_ctx(request.param):
+ yield

MODIFY
test/test_retry.py
test/test_retry.py
@@ -332,6 +332,16 @@ class TestRetry(object):
("Mon Jun 3 11:30:12 2019", True, 1812),
],
)
+ @pytest.mark.parametrize(
+ "stub_timezone",
+ [
+ "UTC",
+ "Asia/Jerusalem",
+ None,
+ ],
+ indirect=True,
+ )
+ @pytest.mark.usefixtures("stub_timezone")
def test_respect_retry_after_header_sleep(
self, retry_after_header, respect_retry_after_header, sleep_duration
):

ADD
test/tz_stub.py
+from contextlib import contextmanager
+import time
+import datetime
+import os
+import pytest
+from dateutil import tz
+
+
+@contextmanager
+def stub_timezone_ctx(tzname):
+ """
+ Switch to a locally-known timezone specified by `tzname`.
+ On exit, restore the previous timezone.
+ If `tzname` is `None`, do nothing.
+ """
+ if tzname is None:
+ yield
+ return
+
+ # Only supported on Unix
+ if not hasattr(time, "tzset"):
+ pytest.skip("Timezone patching is not supported")
+
+ # Make sure the new timezone exists, at least in dateutil
+ new_tz = tz.gettz(tzname)
+ if new_tz is None:
+ raise ValueError("Invalid timezone specified: %r" % (tzname,))
+
+ # Get the current timezone
+ local_tz = tz.tzlocal()
+ if local_tz is None:
+ raise EnvironmentError("Cannot determine current timezone")
+ old_tzname = datetime.datetime.now(local_tz).tzname()
+
+ os.environ["TZ"] = tzname
+ time.tzset()
+ yield
+ os.environ["TZ"] = old_tzname
+ time.tzset()

Always assume UTC if no timezone in Retry-After