Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assume UTC timezone if none specified in Retry-After header #1935

Merged
merged 5 commits into from Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions dev-requirements.txt
Expand Up @@ -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
7 changes: 7 additions & 0 deletions src/urllib3/util/retry.py
Expand Up @@ -269,6 +269,13 @@ def parse_retry_after(self, retry_after):
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
hodbn marked this conversation as resolved.
Show resolved Hide resolved
# 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()

Expand Down
11 changes: 11 additions & 0 deletions test/conftest.py
Expand Up @@ -8,6 +8,8 @@
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
Expand Down Expand Up @@ -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
10 changes: 10 additions & 0 deletions test/test_retry.py
Expand Up @@ -332,6 +332,16 @@ def test_respect_retry_after_header_propagated(self, respect_retry_after_header)
("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
):
Expand Down
39 changes: 39 additions & 0 deletions test/tz_stub.py
@@ -0,0 +1,39 @@
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()