blob: 70d74aeb10790882cc089addc6ac4e92a4ad4981 [file] [log] [blame]
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
import logging
import urlparse
_NO_RETRY_CODES = [200, 302, 401, 403, 404, 409, 501]
_RETRIABLE_EXCEPTIONS = []
class HttpInterceptorBase(object):
"""A modifying interceptor for http clients.
This is a base class for interceptors that can intercept and modify:
- A request before it is made
- A response before it is returned to the caller
- An exception before it is raised to the caller
It is expected that the caller will use the returned values for each of these
methods instead of the ones originally sent to them.
"""
def __init__(self, no_retry_codes=None, retriable_exceptions=None):
self.no_retry_codes = no_retry_codes or _NO_RETRY_CODES
self._retriable_exceptions = tuple(retriable_exceptions or
_RETRIABLE_EXCEPTIONS)
def GetAuthenticationHeaders(self, request):
"""An interceptor can override this method to produce auth headers.
The http client is expected to call this function for every request and
update the request's headers with the ones this function provides.
Args:
- request(dict): A dict with the relevant fields, such as 'url' and
'headers'
Returns: a dict containing headers to be set to the request before sending.
"""
_ = request
return {}
def OnRequest(self, request):
"""Override this method to examine and/or tweak requests before sending.
The http client is expected to call this method on every request and use its
return value instead.
Args:
- request(dict): A dict with the relevant fields, such as 'url' and
'headers'
Returns: A request dict with possibly modified values.
"""
return request
def OnResponse(self, request, response):
"""Override to check and/or tweak status/body before returning to caller.
Also, decide whether the request should be retried.
The http client is expected to call this method after a response is received
and before any retry logic is applied. The client is expected to return to
the caller the return value of this method, modulo any retry logic that the
client may implement.
Args:
- request(dict): A dict with the relevant fields, such as 'url' and
'headers' for the request that was sent to obtain the response.
- response(dict): A dict with 'status_code' and 'content' fields.
Returns: A response dict with the same values (or possibly modified ones) as
the 'response' arg, **and a boolean indicating whether to retry**
"""
_ = request
return response, response.get('status_code') not in self.no_retry_codes
def OnException(self, request, exception, can_retry):
"""Override to check and/or tweak an exception raised when sending request.
The http client is expected to call this method with any exception raised
when sending the request, if this method returns an exception, the client
will raise it to the caller; if None is returned, the client may retry.
Args:
- request(dict): A dict with the relevant fields, such as 'url' and
'headers' for the request that was sent to obtain the response.
- exception(Exception): The exception raised by the underlying call.
- can_retry(bool): Whether the caller will retry if the interceptor
swallows the exception. Useful to take action on persistent exceptions
and not on transient ones.
Retruns: An exception to be raised to the caller or None.
"""
_ = request
if can_retry and isinstance(exception, self._retriable_exceptions):
# Only swallow a known exception when can_retry is true.
return None
return exception
@staticmethod
def GetHost(url):
"""Standarized way for interceptors to get the host from the requested urls.
The point of having this here instead of making each interceptor parse the
url, is that we may have a single point to use special-casing for some hosts
and/or paths.
Args:
- url(str): A string containing the requested url.
Returns:
The hostname part of the url(str), None if url is empty or None.
"""
host = None
if url:
host = urlparse.urlparse(url).hostname
return host
class LoggingInterceptor(HttpInterceptorBase):
"""A minimal interceptor that logs status code and url."""
def __init__(self, *args, **kwargs):
self.no_error_logging_statuses = kwargs.pop('no_error_logging_statuses', [])
super(LoggingInterceptor, self).__init__(*args, **kwargs)
def OnResponse(self, request, response):
if response.get('status_code') != 200 and response.get(
'status_code') not in self.no_error_logging_statuses:
logging.info(
'request to %s responded with %d http status, headers %s, and content %s', # pylint: disable=line-too-long
request.get('url'), response.get('status_code', 0),
json.dumps(response.get('headers', {}).items()),
response.get('content', ''))
# Call the base's OnResponse to keep the retry functionality.
return super(LoggingInterceptor, self).OnResponse(request, response)
def OnException(self, request, exception, can_retry):
if can_retry:
logging.warning('got exception %s("%s") for url %s', type(exception),
exception.message, request.get('url'))
else:
logging.exception('got exception %s("%s") for url %s', type(exception),
exception.message, request.get('url'))
return super(LoggingInterceptor, self).OnException(request, exception,
can_retry)