| # 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) |