docs: async def http_fetch(self, url,
                           method='GET',
                           request_headers=None,
                           body=None,
                           content_type=CONTENT_TYPE_MSGPACK,
                           follow_redirects=False,
                           max_redirects=MAX_REDIRECTS,
                           connect_timeout=DEFAULT_CONNECT_TIMEOUT,
                           request_timeout=DEFAULT_REQUEST_TIMEOUT,
                           max_http_attempts=MAX_HTTP_RETRIES,
                           auth_username=None,
                           auth_password=None,
                           user_agent=None,
                           validate_cert=True,
                           allow_nonstandard_methods=False,
                           dont_retry=None):
          """Perform a HTTP request
  
          Will retry up to ``self.MAX_HTTP_RETRIES`` times.
  
          :param str url: The URL for the request
          :param str method: The HTTP request method, defaults to ``GET``
          :param dict request_headers: Headers to include in the HTTP request
          :param mixed body: The HTTP request body to send with the request
          :param content_type: The mime type to use for requests & responses.
              Defaults to ``application/msgpack``
          :type content_type: :py:class:`~ietfparse.datastructures.ContentType`
              or str
          :param bool follow_redirects: Follow HTTP redirects when received
          :param int max_redirects: Maximum number of redirects to follow,
              default is 5
          :param float connect_timeout: Timeout for initial connection in
              seconds, default 20 seconds
          :param float request_timeout:  Timeout for entire request in seconds,
              default 20 seconds
          :param int max_http_attempts: Maximum number of times to retry
              a request, default is 3 attempts
          :param str auth_username: Username for HTTP authentication
          :param str auth_password: Password for HTTP authentication
          :param str user_agent: The str used for the ``User-Agent`` header,
              default used if unspecified.
          :param bool validate_cert: For HTTPS requests, validate the server's
              certificate? Default is True
          :param bool allow_nonstandard_methods: Allow methods that don't adhere
              to the HTTP spec.
          :param set dont_retry: A list of status codes that will not be retried
              if an error is returned. Default: set({})
          :rtype: HTTPResponse
  
          """
          response = HTTPResponse()
  
          request_headers = self._http_req_apply_default_headers(
              request_headers, content_type, body)
  
          if body:
              body = self._http_req_body_serialize(
                  body, request_headers['Content-Type'])
  
          if not dont_retry:
              dont_retry = set({})
  
          client = httpclient.AsyncHTTPClient()
  
          # Workaround for Tornado defect.
          if hasattr(client, 'max_clients') and os.getenv('HTTP_MAX_CLIENTS'):
              client.max_clients = int(os.getenv('HTTP_MAX_CLIENTS'))
  
          for attempt in range(0, max_http_attempts):
              LOGGER.debug('%s %s (Attempt %i of %i) %r',
                           method, url, attempt + 1, max_http_attempts,
                           request_headers)
              if attempt > 0:
                  request_headers['X-Retry-Attempt'] = str(attempt + 1)
              try:
                  resp = await client.fetch(
                      url,
                      method=method,
                      headers=request_headers,
                      body=body,
                      auth_username=auth_username,
                      auth_password=auth_password,
                      connect_timeout=connect_timeout,
                      request_timeout=request_timeout,
                      user_agent=user_agent or self._http_req_user_agent(),
                      follow_redirects=follow_redirects,
                      max_redirects=max_redirects,
                      raise_error=False,
                      validate_cert=validate_cert,
                      allow_nonstandard_methods=allow_nonstandard_methods)
              except (ConnectionError,
                      CurlError,
                      OSError,
                      socket.gaierror) as error:
                  response.append_exception(error)
                  LOGGER.warning(
                      'HTTP Request Error for %s to %s attempt %i of %i: %s',
                      method, url, attempt + 1, max_http_attempts, error)
                  continue
  
              # Keep track of each response
              response.append_response(resp)
  
              # If the response is ok, finish and exit
              if response.ok:
                  response.finish()
                  return response
              elif resp.code in dont_retry:
                  break
              elif resp.code in {423, 429}:
                  await self._http_resp_rate_limited(resp)
              elif resp.code < 500:
                  LOGGER.debug('HTTP Response Error for %s to %s'
                               'attempt %i of %i (%s): %s',
                               method, url, resp.code, attempt + 1,
                               max_http_attempts, response.body)
                  response.finish()
                  return response
  
              LOGGER.warning(
                  'HTTP Error for %s to %s, attempt %i of %i (%s): %s',
                  method, url, attempt + 1, max_http_attempts, resp.code,
                  response.body)
  
          LOGGER.warning('HTTP %s to %s failed after %i attempts', method, url,
                         max_http_attempts)
          response.finish()
          return response