Skip to content

Commit 397c2e1

Browse files
rianhunterbraincore
authored andcommitted
Handle Half-open TCP sockets
If the host machine that the Dropbox client SDK is running on misses a FIN packet while it's waiting for a response from the server it will hang forever by default. This can happen if an intermediate router transiently goes down, or if the host is suspended via ACPI power management. This problem is common and widespread and documented here: https://en.wikipedia.org/wiki/TCP_half-open#RFC_793 A thorough description of the problem can be found here: http://blog.stephencleary.com/2009/05/detection-of-half-open-dropped.html This change makes all HTTP API requests instead use a default timeout of 30 seconds instead of unconditionally blocking forever. It adds a special case for files_list_folder_longpoll which may have a user-defined timeout.
1 parent a8282f9 commit 397c2e1

File tree

1 file changed

+51
-9
lines changed

1 file changed

+51
-9
lines changed

dropbox/dropbox.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import requests
1818

19-
from . import stone_serializers
19+
from . import files, stone_serializers
2020
from .auth import (
2121
AuthError_validator,
2222
RateLimitError_validator,
@@ -123,13 +123,17 @@ class _DropboxTransport(object):
123123
# the HTTP body.
124124
_ROUTE_STYLE_RPC = 'rpc'
125125

126+
# This is the default longest time we'll block on receiving data from the server
127+
_DEFAULT_TIMEOUT = 30
128+
126129
def __init__(self,
127130
oauth2_access_token,
128131
max_retries_on_error=4,
129132
max_retries_on_rate_limit=None,
130133
user_agent=None,
131134
session=None,
132-
headers=None):
135+
headers=None,
136+
timeout=_DEFAULT_TIMEOUT):
133137
"""
134138
:param str oauth2_access_token: OAuth2 access token for making client
135139
requests.
@@ -147,6 +151,10 @@ def __init__(self,
147151
:func:`create_session`.
148152
:type session: :class:`requests.sessions.Session`
149153
:param dict headers: Additional headers to add to requests.
154+
:param Optional[float] timeout: Maximum duration in seconds that
155+
client will wait when receiving data from server. After the timeout
156+
the client will give up on connection. If `None`, client will
157+
wait forever. Defaults to 30 seconds.
150158
"""
151159
assert len(oauth2_access_token) > 0, \
152160
'OAuth2 access token cannot be empty.'
@@ -185,11 +193,14 @@ def __init__(self,
185193
self._HOST_CONTENT: self._api_content_hostname,
186194
self._HOST_NOTIFY: self._api_notify_hostname}
187195

196+
self._timeout = timeout
197+
188198
def request(self,
189199
route,
190200
namespace,
191201
request_arg,
192-
request_binary):
202+
request_binary,
203+
timeout=None):
193204
"""
194205
Makes a request to the Dropbox API and in the process validates that
195206
the route argument and result are the expected data types. The
@@ -204,18 +215,36 @@ def request(self,
204215
validator specified by route.arg_type.
205216
:param request_binary: String or file pointer representing the binary
206217
payload. Use None if there is no binary payload.
218+
:param Optional[float] timeout: Maximum duration in seconds that
219+
client will wait when receiving data from server. After the timeout
220+
the client will give up on connection. If `None`, client will
221+
wait forever. Defaults to `None`.
207222
:return: The route's result.
208223
"""
209224
host = route.attrs['host'] or 'api'
210225
route_name = namespace + '/' + route.name
211226
route_style = route.attrs['style'] or 'rpc'
212227
serialized_arg = stone_serializers.json_encode(route.arg_type,
213228
request_arg)
229+
230+
231+
if (timeout is None and
232+
route == files.list_folder_longpoll):
233+
# The client normally sends a timeout value to the
234+
# longpoll route. The server will respond after
235+
# <timeout> + random(0, 90) seconds. We increase the
236+
# socket timeout to the longpoll timeout value plus 90
237+
# seconds so that we don't cut the server response short
238+
# due to a shorter socket timeout.
239+
# NB: This is done here because base.py is auto-generated
240+
timeout = request_arg.timeout + 90
241+
214242
res = self.request_json_string_with_retry(host,
215243
route_name,
216244
route_style,
217245
serialized_arg,
218-
request_binary)
246+
request_binary,
247+
timeout=timeout)
219248
decoded_obj_result = json.loads(res.obj_result)
220249
if isinstance(res, RouteResult):
221250
returned_data_type = route.result_type
@@ -249,7 +278,8 @@ def request_json_object(self,
249278
route_name,
250279
route_style,
251280
request_arg,
252-
request_binary):
281+
request_binary,
282+
timeout=None):
253283
"""
254284
Makes a request to the Dropbox API, taking a JSON-serializable Python
255285
object as an argument, and returning one as a response.
@@ -261,14 +291,19 @@ def request_json_object(self,
261291
the argument for the route.
262292
:param request_binary: String or file pointer representing the binary
263293
payload. Use None if there is no binary payload.
294+
:param Optional[float] timeout: Maximum duration in seconds that
295+
client will wait when receiving data from server. After the timeout
296+
the client will give up on connection. If `None`, client will
297+
wait forever. Defaults to `None`.
264298
:return: The route's result as a JSON-serializable Python object.
265299
"""
266300
serialized_arg = json.dumps(request_arg)
267301
res = self.request_json_string_with_retry(host,
268302
route_name,
269303
route_style,
270304
serialized_arg,
271-
request_binary)
305+
request_binary,
306+
timeout=timeout)
272307
# This can throw a ValueError if the result is not deserializable,
273308
# but that would be completely unexpected.
274309
deserialized_result = json.loads(res.obj_result)
@@ -282,7 +317,8 @@ def request_json_string_with_retry(self,
282317
route_name,
283318
route_style,
284319
request_json_arg,
285-
request_binary):
320+
request_binary,
321+
timeout=None):
286322
"""
287323
See :meth:`request_json_object` for description of parameters.
288324
@@ -298,7 +334,8 @@ def request_json_string_with_retry(self,
298334
route_name,
299335
route_style,
300336
request_json_arg,
301-
request_binary)
337+
request_binary,
338+
timeout=timeout)
302339
except InternalServerError as e:
303340
attempt += 1
304341
if attempt <= self._max_retries_on_error:
@@ -327,7 +364,8 @@ def request_json_string(self,
327364
func_name,
328365
route_style,
329366
request_json_arg,
330-
request_binary):
367+
request_binary,
368+
timeout=None):
331369
"""
332370
See :meth:`request_json_string_with_retry` for description of
333371
parameters.
@@ -365,11 +403,15 @@ def request_json_string(self,
365403
else:
366404
raise ValueError('Unknown operation style: %r' % route_style)
367405

406+
if timeout is None:
407+
timeout = self._timeout
408+
368409
r = self._session.post(url,
369410
headers=headers,
370411
data=body,
371412
stream=stream,
372413
verify=True,
414+
timeout=timeout,
373415
)
374416

375417
request_id = r.headers.get('x-dropbox-request-id')

0 commit comments

Comments
 (0)