<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 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