Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1372)

Side by Side Diff: third_party/google-endpoints/apitools/base/py/http_wrapper.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 #
3 # Copyright 2015 Google Inc.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """HTTP wrapper for apitools.
18
19 This library wraps the underlying http library we use, which is
20 currently httplib2.
21 """
22
23 import collections
24 import contextlib
25 import logging
26 import socket
27 import time
28
29 import httplib2
30 import oauth2client
31 import six
32 from six.moves import http_client
33 from six.moves.urllib import parse
34
35 from apitools.base.py import exceptions
36 from apitools.base.py import util
37
38 __all__ = [
39 'CheckResponse',
40 'GetHttp',
41 'HandleExceptionsAndRebuildHttpConnections',
42 'MakeRequest',
43 'RebuildHttpConnections',
44 'Request',
45 'Response',
46 'RethrowExceptionHandler',
47 ]
48
49
50 # 308 and 429 don't have names in httplib.
51 RESUME_INCOMPLETE = 308
52 TOO_MANY_REQUESTS = 429
53 _REDIRECT_STATUS_CODES = (
54 http_client.MOVED_PERMANENTLY,
55 http_client.FOUND,
56 http_client.SEE_OTHER,
57 http_client.TEMPORARY_REDIRECT,
58 RESUME_INCOMPLETE,
59 )
60
61 # http: An httplib2.Http instance.
62 # http_request: A http_wrapper.Request.
63 # exc: Exception being raised.
64 # num_retries: Number of retries consumed; used for exponential backoff.
65 ExceptionRetryArgs = collections.namedtuple(
66 'ExceptionRetryArgs', ['http', 'http_request', 'exc', 'num_retries',
67 'max_retry_wait'])
68
69
70 @contextlib.contextmanager
71 def _Httplib2Debuglevel(http_request, level, http=None):
72 """Temporarily change the value of httplib2.debuglevel, if necessary.
73
74 If http_request has a `loggable_body` distinct from `body`, then we
75 need to prevent httplib2 from logging the full body. This sets
76 httplib2.debuglevel for the duration of the `with` block; however,
77 that alone won't change the value of existing HTTP connections. If
78 an httplib2.Http object is provided, we'll also change the level on
79 any cached connections attached to it.
80
81 Args:
82 http_request: a Request we're logging.
83 level: (int) the debuglevel for logging.
84 http: (optional) an httplib2.Http whose connections we should
85 set the debuglevel on.
86
87 Yields:
88 None.
89 """
90 if http_request.loggable_body is None:
91 yield
92 return
93 old_level = httplib2.debuglevel
94 http_levels = {}
95 httplib2.debuglevel = level
96 if http is not None:
97 for connection_key, connection in http.connections.items():
98 # httplib2 stores two kinds of values in this dict, connection
99 # classes and instances. Since the connection types are all
100 # old-style classes, we can't easily distinguish by connection
101 # type -- so instead we use the key pattern.
102 if ':' not in connection_key:
103 continue
104 http_levels[connection_key] = connection.debuglevel
105 connection.set_debuglevel(level)
106 yield
107 httplib2.debuglevel = old_level
108 if http is not None:
109 for connection_key, old_level in http_levels.items():
110 if connection_key in http.connections:
111 http.connections[connection_key].set_debuglevel(old_level)
112
113
114 class Request(object):
115
116 """Class encapsulating the data for an HTTP request."""
117
118 def __init__(self, url='', http_method='GET', headers=None, body=''):
119 self.url = url
120 self.http_method = http_method
121 self.headers = headers or {}
122 self.__body = None
123 self.__loggable_body = None
124 self.body = body
125
126 @property
127 def loggable_body(self):
128 return self.__loggable_body
129
130 @loggable_body.setter
131 def loggable_body(self, value):
132 if self.body is None:
133 raise exceptions.RequestError(
134 'Cannot set loggable body on request with no body')
135 self.__loggable_body = value
136
137 @property
138 def body(self):
139 return self.__body
140
141 @body.setter
142 def body(self, value):
143 """Sets the request body; handles logging and length measurement."""
144 self.__body = value
145 if value is not None:
146 # Avoid calling len() which cannot exceed 4GiB in 32-bit python.
147 body_length = getattr(
148 self.__body, 'length', None) or len(self.__body)
149 self.headers['content-length'] = str(body_length)
150 else:
151 self.headers.pop('content-length', None)
152 # This line ensures we don't try to print large requests.
153 if not isinstance(value, (type(None), six.string_types)):
154 self.loggable_body = '<media body>'
155
156
157 # Note: currently the order of fields here is important, since we want
158 # to be able to pass in the result from httplib2.request.
159 class Response(collections.namedtuple(
160 'HttpResponse', ['info', 'content', 'request_url'])):
161
162 """Class encapsulating data for an HTTP response."""
163 __slots__ = ()
164
165 def __len__(self):
166 return self.length
167
168 @property
169 def length(self):
170 """Return the length of this response.
171
172 We expose this as an attribute since using len() directly can fail
173 for responses larger than sys.maxint.
174
175 Returns:
176 Response length (as int or long)
177 """
178 def ProcessContentRange(content_range):
179 _, _, range_spec = content_range.partition(' ')
180 byte_range, _, _ = range_spec.partition('/')
181 start, _, end = byte_range.partition('-')
182 return int(end) - int(start) + 1
183
184 if '-content-encoding' in self.info and 'content-range' in self.info:
185 # httplib2 rewrites content-length in the case of a compressed
186 # transfer; we can't trust the content-length header in that
187 # case, but we *can* trust content-range, if it's present.
188 return ProcessContentRange(self.info['content-range'])
189 elif 'content-length' in self.info:
190 return int(self.info.get('content-length'))
191 elif 'content-range' in self.info:
192 return ProcessContentRange(self.info['content-range'])
193 return len(self.content)
194
195 @property
196 def status_code(self):
197 return int(self.info['status'])
198
199 @property
200 def retry_after(self):
201 if 'retry-after' in self.info:
202 return int(self.info['retry-after'])
203
204 @property
205 def is_redirect(self):
206 return (self.status_code in _REDIRECT_STATUS_CODES and
207 'location' in self.info)
208
209
210 def CheckResponse(response):
211 if response is None:
212 # Caller shouldn't call us if the response is None, but handle anyway.
213 raise exceptions.RequestError(
214 'Request to url %s did not return a response.' %
215 response.request_url)
216 elif (response.status_code >= 500 or
217 response.status_code == TOO_MANY_REQUESTS):
218 raise exceptions.BadStatusCodeError.FromResponse(response)
219 elif response.retry_after:
220 raise exceptions.RetryAfterError.FromResponse(response)
221
222
223 def RebuildHttpConnections(http):
224 """Rebuilds all http connections in the httplib2.Http instance.
225
226 httplib2 overloads the map in http.connections to contain two different
227 types of values:
228 { scheme string: connection class } and
229 { scheme + authority string : actual http connection }
230 Here we remove all of the entries for actual connections so that on the
231 next request httplib2 will rebuild them from the connection types.
232
233 Args:
234 http: An httplib2.Http instance.
235 """
236 if getattr(http, 'connections', None):
237 for conn_key in list(http.connections.keys()):
238 if ':' in conn_key:
239 del http.connections[conn_key]
240
241
242 def RethrowExceptionHandler(*unused_args):
243 # pylint: disable=misplaced-bare-raise
244 raise
245
246
247 def HandleExceptionsAndRebuildHttpConnections(retry_args):
248 """Exception handler for http failures.
249
250 This catches known failures and rebuilds the underlying HTTP connections.
251
252 Args:
253 retry_args: An ExceptionRetryArgs tuple.
254 """
255 # If the server indicates how long to wait, use that value. Otherwise,
256 # calculate the wait time on our own.
257 retry_after = None
258
259 # Transport failures
260 if isinstance(retry_args.exc, (http_client.BadStatusLine,
261 http_client.IncompleteRead,
262 http_client.ResponseNotReady)):
263 logging.debug('Caught HTTP error %s, retrying: %s',
264 type(retry_args.exc).__name__, retry_args.exc)
265 elif isinstance(retry_args.exc, socket.error):
266 logging.debug('Caught socket error, retrying: %s', retry_args.exc)
267 elif isinstance(retry_args.exc, socket.gaierror):
268 logging.debug(
269 'Caught socket address error, retrying: %s', retry_args.exc)
270 elif isinstance(retry_args.exc, socket.timeout):
271 logging.debug(
272 'Caught socket timeout error, retrying: %s', retry_args.exc)
273 elif isinstance(retry_args.exc, httplib2.ServerNotFoundError):
274 logging.debug(
275 'Caught server not found error, retrying: %s', retry_args.exc)
276 elif isinstance(retry_args.exc, ValueError):
277 # oauth2client tries to JSON-decode the response, which can result
278 # in a ValueError if the response was invalid. Until that is fixed in
279 # oauth2client, need to handle it here.
280 logging.debug('Response content was invalid (%s), retrying',
281 retry_args.exc)
282 elif (isinstance(retry_args.exc,
283 oauth2client.client.HttpAccessTokenRefreshError) and
284 (retry_args.exc.status == TOO_MANY_REQUESTS or
285 retry_args.exc.status >= 500)):
286 logging.debug(
287 'Caught transient credential refresh error (%s), retrying',
288 retry_args.exc)
289 elif isinstance(retry_args.exc, exceptions.RequestError):
290 logging.debug('Request returned no response, retrying')
291 # API-level failures
292 elif isinstance(retry_args.exc, exceptions.BadStatusCodeError):
293 logging.debug('Response returned status %s, retrying',
294 retry_args.exc.status_code)
295 elif isinstance(retry_args.exc, exceptions.RetryAfterError):
296 logging.debug('Response returned a retry-after header, retrying')
297 retry_after = retry_args.exc.retry_after
298 else:
299 raise # pylint: disable=misplaced-bare-raise
300 RebuildHttpConnections(retry_args.http)
301 logging.debug('Retrying request to url %s after exception %s',
302 retry_args.http_request.url, retry_args.exc)
303 time.sleep(
304 retry_after or util.CalculateWaitForRetry(
305 retry_args.num_retries, max_wait=retry_args.max_retry_wait))
306
307
308 def MakeRequest(http, http_request, retries=7, max_retry_wait=60,
309 redirections=5,
310 retry_func=HandleExceptionsAndRebuildHttpConnections,
311 check_response_func=CheckResponse):
312 """Send http_request via the given http, performing error/retry handling.
313
314 Args:
315 http: An httplib2.Http instance, or a http multiplexer that delegates to
316 an underlying http, for example, HTTPMultiplexer.
317 http_request: A Request to send.
318 retries: (int, default 7) Number of retries to attempt on retryable
319 replies (such as 429 or 5XX).
320 max_retry_wait: (int, default 60) Maximum number of seconds to wait
321 when retrying.
322 redirections: (int, default 5) Number of redirects to follow.
323 retry_func: Function to handle retries on exceptions. Arguments are
324 (Httplib2.Http, Request, Exception, int num_retries).
325 check_response_func: Function to validate the HTTP response.
326 Arguments are (Response, response content, url).
327
328 Raises:
329 InvalidDataFromServerError: if there is no response after retries.
330
331 Returns:
332 A Response object.
333
334 """
335 retry = 0
336 while True:
337 try:
338 return _MakeRequestNoRetry(
339 http, http_request, redirections=redirections,
340 check_response_func=check_response_func)
341 # retry_func will consume the exception types it handles and raise.
342 # pylint: disable=broad-except
343 except Exception as e:
344 retry += 1
345 if retry >= retries:
346 raise
347 else:
348 retry_func(ExceptionRetryArgs(
349 http, http_request, e, retry, max_retry_wait))
350
351
352 def _MakeRequestNoRetry(http, http_request, redirections=5,
353 check_response_func=CheckResponse):
354 """Send http_request via the given http.
355
356 This wrapper exists to handle translation between the plain httplib2
357 request/response types and the Request and Response types above.
358
359 Args:
360 http: An httplib2.Http instance, or a http multiplexer that delegates to
361 an underlying http, for example, HTTPMultiplexer.
362 http_request: A Request to send.
363 redirections: (int, default 5) Number of redirects to follow.
364 check_response_func: Function to validate the HTTP response.
365 Arguments are (Response, response content, url).
366
367 Returns:
368 A Response object.
369
370 Raises:
371 RequestError if no response could be parsed.
372
373 """
374 connection_type = None
375 # Handle overrides for connection types. This is used if the caller
376 # wants control over the underlying connection for managing callbacks
377 # or hash digestion.
378 if getattr(http, 'connections', None):
379 url_scheme = parse.urlsplit(http_request.url).scheme
380 if url_scheme and url_scheme in http.connections:
381 connection_type = http.connections[url_scheme]
382
383 # Custom printing only at debuglevel 4
384 new_debuglevel = 4 if httplib2.debuglevel == 4 else 0
385 with _Httplib2Debuglevel(http_request, new_debuglevel, http=http):
386 info, content = http.request(
387 str(http_request.url), method=str(http_request.http_method),
388 body=http_request.body, headers=http_request.headers,
389 redirections=redirections, connection_type=connection_type)
390
391 if info is None:
392 raise exceptions.RequestError()
393
394 response = Response(info, content, http_request.url)
395 check_response_func(response)
396 return response
397
398
399 _HTTP_FACTORIES = []
400
401
402 def _RegisterHttpFactory(factory):
403 _HTTP_FACTORIES.append(factory)
404
405
406 def GetHttp(**kwds):
407 for factory in _HTTP_FACTORIES:
408 http = factory(**kwds)
409 if http is not None:
410 return http
411 return httplib2.Http(**kwds)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698