Skip to content

Commit 3247137

Browse files
author
James E. Blair
committed
Set timeout on HTTP requests
The requests documentation[0] recommends always setting a timeout for requests in order to avoid indefinite hangs. This can happen if the remote side is unresponsive, or if a network error causes a FIN or RST packet never to arrive. This sets a default connect timeout of 4 seconds and a read timeout of 1 second for all HTTP requests. The default can be overridden in the GitHubSession constructor. The GitHubSession class is also added to the documentation on the GitHub Object page since it is closely associated with it. [0] http://docs.python-requests.org/en/master/user/quickstart/#timeouts
1 parent b5d51ce commit 3247137

4 files changed

Lines changed: 72 additions & 3 deletions

File tree

AUTHORS.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,5 @@ Contributors
174174
- Raphael Vogel (@RaphaelVogel)
175175

176176
- Andreas Burger (@AndreasBurger)
177+
178+
- James E. Blair (@jeblair)

docs/source/api-reference/github.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@ GitHubStatus Object
3434

3535
.. autoclass:: github3.github.GitHubStatus
3636
:members:
37+
38+
39+
GitHubSession Object
40+
====================
41+
42+
.. autoclass:: github3.session.GitHubSession

src/github3/session.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,40 @@ def __call__(self, request):
6464

6565

6666
class GitHubSession(requests.Session):
67-
"""Our slightly specialized Session object."""
67+
"""Our slightly specialized Session object.
68+
69+
Normally this is created automatically by
70+
:class:`~github3.github.GitHub`. To use alternate values for
71+
network timeouts, this class can be instantiated directly and
72+
passed to the GitHub object. For example:
73+
74+
.. code-block:: python
75+
76+
gh = github.GitHub(session=session.GitHubSession(
77+
default_connect_timeout=T, default_read_timeout=N))
78+
79+
:param default_connect_timeout:
80+
the number of seconds to wait when establishing a connection to
81+
GitHub
82+
:type default_connect_timeout:
83+
float
84+
:param default_read_timeout:
85+
the number of seconds to wait for a response from GitHub
86+
:type default_read_timeout:
87+
float
88+
"""
6889

6990
auth = None
7091
__attrs__ = requests.Session.__attrs__ + [
7192
"base_url",
7293
"two_factor_auth_cb",
7394
]
7495

75-
def __init__(self):
96+
def __init__(self, default_connect_timeout=4, default_read_timeout=1):
7697
"""Slightly modify how we initialize our session."""
7798
super(GitHubSession, self).__init__()
99+
self.default_connect_timeout = default_connect_timeout
100+
self.default_read_timeout = default_read_timeout
78101
self.headers.update(
79102
{
80103
# Only accept JSON responses
@@ -91,6 +114,11 @@ def __init__(self):
91114
self.two_factor_auth_cb = None
92115
self.request_counter = 0
93116

117+
@property
118+
def timeout(self):
119+
"""Return the timeout tuple as expected by Requests"""
120+
return (self.default_connect_timeout, self.default_read_timeout)
121+
94122
def basic_auth(self, username, password):
95123
"""Set the Basic Auth credentials on this Session.
96124
@@ -137,6 +165,7 @@ def oauth2_auth(self, client_id, client_secret):
137165

138166
def request(self, *args, **kwargs):
139167
"""Make a request, count it, and handle 2FA if necessary."""
168+
kwargs.setdefault('timeout', self.timeout)
140169
response = super(GitHubSession, self).request(*args, **kwargs)
141170
self.request_counter += 1
142171
if requires_2fa(response) and self.two_factor_auth_cb:

tests/unit/test_github_session.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@ def test_has_default_headers(self):
3030
assert "User-Agent" in s.headers
3131
assert s.headers["User-Agent"].startswith("github3.py/")
3232

33+
@mock.patch.object(requests.Session, 'request')
34+
def test_default_timeout(self, request_mock):
35+
"""Test that default timeout values are used"""
36+
response = mock.Mock()
37+
response.configure_mock(status_code=200, headers={})
38+
request_mock.return_value = response
39+
s = self.build_session()
40+
r = s.get("http://example.com")
41+
assert r is response
42+
request_mock.assert_called_once_with(
43+
"GET", "http://example.com", allow_redirects=True,
44+
timeout=(4, 1)
45+
)
46+
47+
@mock.patch.object(requests.Session, 'request')
48+
def test_custom_timeout(self, request_mock):
49+
"""Test that custom timeout values are used"""
50+
response = mock.Mock()
51+
response.configure_mock(status_code=200, headers={})
52+
request_mock.return_value = response
53+
s = session.GitHubSession(
54+
default_connect_timeout=300,
55+
default_read_timeout=400
56+
)
57+
r = s.get("http://example.com")
58+
assert r is response
59+
request_mock.assert_called_once_with(
60+
"GET", "http://example.com", allow_redirects=True,
61+
timeout=(300, 400)
62+
)
63+
3364
def test_build_url(self):
3465
"""Test that GitHubSessions build basic URLs"""
3566
s = self.build_session()
@@ -117,7 +148,8 @@ def test_request_ignores_responses_that_do_not_require_2fa(
117148
r = s.get("http://example.com")
118149
assert r is response
119150
request_mock.assert_called_once_with(
120-
"GET", "http://example.com", allow_redirects=True
151+
"GET", "http://example.com", allow_redirects=True,
152+
timeout=(4, 1)
121153
)
122154

123155
@mock.patch.object(requests.Session, "request")

0 commit comments

Comments
 (0)