core.com.qiniu.http.AmazonHttpClient.java Source code

Java tutorial

Introduction

Here is the source code for core.com.qiniu.http.AmazonHttpClient.java

Source

/*
 * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package core.com.qiniu.http;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import core.com.qiniu.AmazonClientException;
import core.com.qiniu.AmazonServiceException;
import core.com.qiniu.AmazonWebServiceRequest;
import core.com.qiniu.AmazonWebServiceResponse;
import core.com.qiniu.ClientConfiguration;
import core.com.qiniu.Request;
import core.com.qiniu.RequestClientOptions;
import core.com.qiniu.Response;
import core.com.qiniu.ResponseMetadata;
import core.com.qiniu.SDKGlobalConfiguration;
import core.com.qiniu.auth.AWSCredentials;
import core.com.qiniu.auth.Signer;
import core.com.qiniu.handler.CredentialsRequestHandler;
import core.com.qiniu.handler.RequestHandler2;
import core.com.qiniu.internal.CRC32MismatchException;
import core.com.qiniu.metrics.RequestMetricCollector;
import core.com.qiniu.retry.RetryPolicy;
import core.com.qiniu.retry.RetryUtils;
import core.com.qiniu.util.AWSRequestMetrics;
import core.com.qiniu.util.DateUtils;
import core.com.qiniu.util.TimingInfo;

public class AmazonHttpClient {

    private static final String HEADER_USER_AGENT = "User-Agent";
    private static final String HEADER_SDK_TRANSACTION_ID = "aws-sdk-invocation-id";
    private static final String HEADER_SDK_RETRY_INFO = "aws-sdk-retry";

    /**
     * Logger providing detailed information on requests/responses. Users can
     * enable this logger to get access to AWS request IDs for responses,
     * individual requests and parameters sent to AWS, etc.
     */
    private static final Log requestLog = LogFactory.getLog("com.amazonaws.request");

    /**
     * Logger for more detailed debugging information, that might not be as
     * useful for end users (ex: HTTP client configuration, etc).
     */
    static final Log log = LogFactory.getLog(AmazonHttpClient.class);

    /** Internal client for sending HTTP requests */
    final HttpClient httpClient;

    /** Client configuration options, such as proxy settings, max retries, etc. */
    final ClientConfiguration config;

    /**
     * A request metric collector used specifically for this http client; or
     * null if there is none. This collector, if specified, always takes
     * precedence over the one specified at the AWS SDK level.
     *
     * @see AwsSdkMetrics
     */
    private final RequestMetricCollector requestMetricCollector;

    private final HttpRequestFactory requestFactory = new HttpRequestFactory();

    /**
     * Constructs a new AWS client using the specified client configuration
     * options (ex: max retry attempts, proxy settings, etc).
     *
     * @param config Configuration options specifying how this client will
     *            communicate with AWS (ex: proxy settings, retry count, etc.).
     */
    public AmazonHttpClient(ClientConfiguration config) {
        this(config, new UrlHttpClient(config));
    }

    /**
     * Constructs a new AWS client using the specified client configuration
     * options (ex: max retry attempts, proxy settings, etc), and request metric
     * collector.
     *
     * @param config Configuration options specifying how this client will
     *            communicate with AWS (ex: proxy settings, retry count, etc.).
     * @param requestMetricCollector client specific request metric collector,
     *            which takes precedence over the one at the AWS SDK level; or
     *            null if there is none.
     */
    @Deprecated
    public AmazonHttpClient(ClientConfiguration config, RequestMetricCollector requestMetricCollector) {
        this(config, new UrlHttpClient(config), requestMetricCollector);
    }

    /**
     * Constructs a new AWS client using the specified client configuration
     * options (ex: max retry attempts, proxy settings, etc), and request metric
     * collector.
     *
     * @param config Configuration options specifying how this client will
     *            communicate with AWS (ex: proxy settings, retry count, etc.).
     * @param httpClient client specific HttpClient
     */
    public AmazonHttpClient(ClientConfiguration config, HttpClient httpClient) {
        this.config = config;
        this.httpClient = httpClient;
        this.requestMetricCollector = null;
    }

    /**
     * Constructs a new AWS client using the specified client configuration
     * options (ex: max retry attempts, proxy settings, etc), and request metric
     * collector.
     *
     * @param config Configuration options specifying how this client will
     *            communicate with AWS (ex: proxy settings, retry count, etc.).
     * @param httpClient client specific HttpClient
     * @param requestMetricCollector client specific request metric collector,
     *            which takes precedence over the one at the AWS SDK level; or
     *            null if there is none.
     */
    @Deprecated
    public AmazonHttpClient(ClientConfiguration config, HttpClient httpClient,
            RequestMetricCollector requestMetricCollector) {
        this.config = config;
        this.httpClient = httpClient;
        this.requestMetricCollector = requestMetricCollector;
    }

    /**
     * Returns additional response metadata for an executed request. Response
     * metadata isn't considered part of the standard results returned by an
     * operation, so it's accessed instead through this diagnostic interface.
     * Response metadata is typically used for troubleshooting issues with AWS
     * support staff when services aren't acting as expected.
     *
     * @param request A previously executed AmazonWebServiceRequest object,
     *            whose response metadata is desired.
     * @return The response metadata for the specified request, otherwise null
     *         if there is no response metadata available for the request.
     * @deprecated ResponseMetadata cache can hold up to 50 requests and
     *             responses in memory and will cause memory issue. This method
     *             now always returns null.
     */
    @Deprecated
    public ResponseMetadata getResponseMetadataForRequest(AmazonWebServiceRequest request) {
        return null;
    }

    /**
     * Executes the request and returns the result.
     *
     * @param request The AmazonWebServices request to send to the remote server
     * @param responseHandler A response handler to accept a successful response
     *            from the remote server
     * @param errorResponseHandler A response handler to accept an unsuccessful
     *            response from the remote server
     * @param executionContext Additional information about the context of this
     *            web service call
     */
    public <T> Response<T> execute(Request<?> request,
            HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler,
            HttpResponseHandler<AmazonServiceException> errorResponseHandler, ExecutionContext executionContext)
            throws AmazonClientException, AmazonServiceException {
        if (executionContext == null)
            throw new AmazonClientException("Internal SDK Error: No execution context parameter specified.");
        List<RequestHandler2> requestHandler2s = requestHandler2s(request, executionContext);
        final AWSRequestMetrics awsRequestMetrics = executionContext.getAwsRequestMetrics();
        Response<T> response = null;
        try {
            response = executeHelper(request, responseHandler, errorResponseHandler, executionContext);
            TimingInfo timingInfo = awsRequestMetrics.getTimingInfo().endTiming();
            afterResponse(request, requestHandler2s, response, timingInfo);
            return response;
        } catch (AmazonClientException e) {
            afterError(request, response, requestHandler2s, e);
            throw e;
        }
    }

    void afterError(Request<?> request, Response<?> response, List<RequestHandler2> requestHandler2s,
            AmazonClientException e) {
        for (RequestHandler2 handler2 : requestHandler2s) {
            handler2.afterError(request, response, e);
        }
    }

    <T> void afterResponse(Request<?> request, List<RequestHandler2> requestHandler2s, Response<T> response,
            TimingInfo timingInfo) {
        for (RequestHandler2 handler2 : requestHandler2s) {
            handler2.afterResponse(request, response);
        }
    }

    List<RequestHandler2> requestHandler2s(Request<?> request, ExecutionContext executionContext) {
        List<RequestHandler2> requestHandler2s = executionContext.getRequestHandler2s();
        if (requestHandler2s == null) {
            return Collections.emptyList();
        }
        // Apply any additional service specific request handlers that need
        // to be run
        for (RequestHandler2 requestHandler2 : requestHandler2s) {
            // If the request handler is a type of CredentialsRequestHandler,
            // then set the credentials in the request handler.
            if (requestHandler2 instanceof CredentialsRequestHandler)
                ((CredentialsRequestHandler) requestHandler2).setCredentials(executionContext.getCredentials());
            requestHandler2.beforeRequest(request);
        }
        return requestHandler2s;
    }

    /**
     * Internal method to execute the HTTP method given.
     *
     * @see AmazonHttpClient#execute(Request, HttpResponseHandler,
     *      HttpResponseHandler)
     * @see AmazonHttpClient#execute(Request, HttpResponseHandler,
     *      HttpResponseHandler, ExecutionContext)
     */
    <T> Response<T> executeHelper(Request<?> request,
            HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler,
            HttpResponseHandler<AmazonServiceException> errorResponseHandler, ExecutionContext executionContext)
            throws AmazonClientException, AmazonServiceException {
        /*
         * Depending on which response handler we end up choosing to handle the
         * HTTP response, it might require us to leave the underlying HTTP
         * connection open, depending on whether or not it reads the complete
         * HTTP response stream from the HTTP connection, or if delays reading
         * any of the content until after a response is returned to the caller.
         */
        boolean leaveHttpConnectionOpen = false;
        AWSRequestMetrics awsRequestMetrics = executionContext.getAwsRequestMetrics();
        /*
         * add the service endpoint to the logs. You can infer service name from
         * service endpoint
         */
        awsRequestMetrics.addProperty(AWSRequestMetrics.Field.ServiceName, request.getServiceName());
        awsRequestMetrics.addProperty(AWSRequestMetrics.Field.ServiceEndpoint, request.getEndpoint());

        // Apply whatever request options we know how to handle, such as
        // user-agent.
        setUserAgent(request);
        request.addHeader(HEADER_SDK_TRANSACTION_ID, UUID.randomUUID().toString());
        int requestCount = 0;
        long lastBackoffDelay = 0;
        URI redirectedURI = null;
        AmazonClientException retriedException = null;

        // Make a copy of the original request params and headers so that we can
        // permute it in this loop and start over with the original every time.
        Map<String, String> originalParameters = new LinkedHashMap<String, String>(request.getParameters());
        Map<String, String> originalHeaders = new HashMap<String, String>(request.getHeaders());
        // mark input stream if supported
        InputStream originalContent = request.getContent();
        if (originalContent != null && originalContent.markSupported()) {
            originalContent.mark(-1);
        }

        final AWSCredentials credentials = executionContext.getCredentials();
        Signer signer = null;
        HttpResponse httpResponse = null;
        HttpRequest httpRequest = null;

        while (true) {
            ++requestCount;
            awsRequestMetrics.setCounter(AWSRequestMetrics.Field.RequestCount, requestCount);
            if (requestCount > 1) { // retry
                request.setParameters(originalParameters);
                request.setHeaders(originalHeaders);
                request.setContent(originalContent);
            }
            if (redirectedURI != null) {
                request.setEndpoint(URI.create(redirectedURI.getScheme() + "://" + redirectedURI.getAuthority()));
                request.setResourcePath(redirectedURI.getPath());
            }

            try {
                if (requestCount > 1) { // retry
                    awsRequestMetrics.startEvent(AWSRequestMetrics.Field.RetryPauseTime);
                    try {
                        lastBackoffDelay = pauseBeforeNextRetry(request.getOriginalRequest(), retriedException,
                                requestCount, config.getRetryPolicy());
                    } finally {
                        awsRequestMetrics.endEvent(AWSRequestMetrics.Field.RetryPauseTime);
                    }
                    InputStream content = request.getContent();
                    if (content != null && content.markSupported()) {
                        content.reset();
                    }
                }
                request.addHeader(HEADER_SDK_RETRY_INFO, (requestCount - 1) + "/" + lastBackoffDelay);

                // Sign the request if a signer was provided
                if (signer == null)
                    signer = executionContext.getSignerByURI(request.getEndpoint());
                if (signer != null && credentials != null) {
                    awsRequestMetrics.startEvent(AWSRequestMetrics.Field.RequestSigningTime);
                    try {
                        signer.sign(request, credentials);
                    } finally {
                        awsRequestMetrics.endEvent(AWSRequestMetrics.Field.RequestSigningTime);
                    }
                }

                if (requestLog.isDebugEnabled()) {
                    requestLog.debug("Sending Request: " + request.toString());
                }

                httpRequest = requestFactory.createHttpRequest(request, config, executionContext);

                retriedException = null;
                awsRequestMetrics.startEvent(AWSRequestMetrics.Field.HttpRequestTime);
                try {
                    httpResponse = httpClient.execute(httpRequest);
                } finally {
                    awsRequestMetrics.endEvent(AWSRequestMetrics.Field.HttpRequestTime);
                }

                if (isRequestSuccessful(httpResponse)) {
                    awsRequestMetrics.addProperty(AWSRequestMetrics.Field.StatusCode, httpResponse.getStatusCode());
                    /*
                     * If we get back any 2xx status code, then we know we
                     * should treat the service call as successful.
                     */
                    leaveHttpConnectionOpen = responseHandler.needsConnectionLeftOpen();
                    T response = handleResponse(request, responseHandler, httpResponse, executionContext);
                    return new Response<T>(response, httpResponse);
                } else if (isTemporaryRedirect(httpResponse)) {
                    /*
                     * S3 sends 307 Temporary Redirects if you try to delete an
                     * EU bucket from the US endpoint. If we get a 307, we'll
                     * point the HTTP method to the redirected location, and let
                     * the next retry deliver the request to the right location.
                     */
                    String redirectedLocation = httpResponse.getHeaders().get("Location");
                    log.debug("Redirecting to: " + redirectedLocation);
                    // set redirect uri and retry
                    redirectedURI = URI.create(redirectedLocation);
                    awsRequestMetrics.addProperty(AWSRequestMetrics.Field.StatusCode, httpResponse.getStatusCode());
                    awsRequestMetrics.addProperty(AWSRequestMetrics.Field.RedirectLocation, redirectedLocation);
                    awsRequestMetrics.addProperty(AWSRequestMetrics.Field.AWSRequestID, null);
                } else {
                    leaveHttpConnectionOpen = errorResponseHandler.needsConnectionLeftOpen();
                    AmazonServiceException ase = handleErrorResponse(request, errorResponseHandler, httpResponse);
                    awsRequestMetrics.addProperty(AWSRequestMetrics.Field.AWSRequestID, ase.getRequestId());
                    awsRequestMetrics.addProperty(AWSRequestMetrics.Field.AWSErrorCode, ase.getErrorCode());
                    awsRequestMetrics.addProperty(AWSRequestMetrics.Field.StatusCode, ase.getStatusCode());

                    if (!shouldRetry(request.getOriginalRequest(), httpRequest.getContent(), ase, requestCount,
                            config.getRetryPolicy())) {
                        throw ase;
                    }

                    // Cache the retryable exception
                    retriedException = ase;
                    /*
                     * Checking for clock skew error again because we don't want
                     * to set the global time offset for every service
                     * exception.
                     */
                    if (RetryUtils.isClockSkewError(ase)) {
                        int timeOffset = parseClockSkewOffset(httpResponse, ase);
                        SDKGlobalConfiguration.setGlobalTimeOffset(timeOffset);
                    }
                    resetRequestAfterError(request, ase);
                }
            } catch (IOException ioe) {
                if (log.isDebugEnabled()) {
                    log.debug("Unable to execute HTTP request: " + ioe.getMessage(), ioe);
                }
                awsRequestMetrics.incrementCounter(AWSRequestMetrics.Field.Exception);
                awsRequestMetrics.addProperty(AWSRequestMetrics.Field.Exception, ioe);
                awsRequestMetrics.addProperty(AWSRequestMetrics.Field.AWSRequestID, null);

                AmazonClientException ace = new AmazonClientException(
                        "Unable to execute HTTP request: " + ioe.getMessage(), ioe);
                if (!shouldRetry(request.getOriginalRequest(), httpRequest.getContent(), ace, requestCount,
                        config.getRetryPolicy())) {
                    throw ace;
                }

                // Cache the retryable exception
                retriedException = ace;
                resetRequestAfterError(request, ioe);
            } catch (RuntimeException e) {
                throw handleUnexpectedFailure(e, awsRequestMetrics);
            } catch (Error e) {
                throw handleUnexpectedFailure(e, awsRequestMetrics);
            } finally {
                /*
                 * Some response handlers need to manually manage the HTTP
                 * connection and will take care of releasing the connection on
                 * their own, but if this response handler doesn't need the
                 * connection left open, we go ahead and release the it to free
                 * up resources.
                 */
                if (!leaveHttpConnectionOpen && httpResponse != null) {
                    try {
                        if (httpResponse.getRawContent() != null) {
                            httpResponse.getRawContent().close();
                        }
                    } catch (IOException e) {
                        log.warn("Cannot close the response content.", e);
                    }
                }
            }
        } /* end while (true) */
    }

    /**
     * Handles an unexpected failure, returning the Throwable instance as given.
     */
    private <T extends Throwable> T handleUnexpectedFailure(T t, AWSRequestMetrics awsRequestMetrics) {
        awsRequestMetrics.incrementCounter(AWSRequestMetrics.Field.Exception);
        awsRequestMetrics.addProperty(AWSRequestMetrics.Field.Exception, t);
        return t;
    }

    /**
     * Resets the specified request, so that it can be sent again, after
     * receiving the specified error. If a problem is encountered with resetting
     * the request, then an AmazonClientException is thrown with the original
     * error as the cause (not an error about being unable to reset the stream).
     *
     * @param request The request being executed that failed and needs to be
     *            reset.
     * @param cause The original error that caused the request to fail.
     * @throws AmazonClientException If the request can't be reset.
     */
    void resetRequestAfterError(Request<?> request, Exception cause) throws AmazonClientException {
        if (request.getContent() == null) {
            return; // no reset needed
        }
        if (!request.getContent().markSupported()) {
            throw new AmazonClientException("Encountered an exception and stream is not resettable", cause);
        }
        try {
            request.getContent().reset();
        } catch (IOException e) {
            // This exception comes from being unable to reset the input stream,
            // so throw the original, more meaningful exception
            throw new AmazonClientException("Encountered an exception and couldn't reset the stream to retry",
                    cause);
        }
    }

    /**
     * Sets a User-Agent for the specified request, taking into account any
     * custom data.
     */
    void setUserAgent(Request<?> request) {
        String userAgent = ClientConfiguration.DEFAULT_USER_AGENT;
        // append request specific user agent marker
        AmazonWebServiceRequest awsreq = request.getOriginalRequest();
        if (awsreq != null) {
            RequestClientOptions opts = awsreq.getRequestClientOptions();
            if (opts != null) {
                String userAgentMarker = opts.getClientMarker(RequestClientOptions.Marker.USER_AGENT);
                if (userAgentMarker != null) {
                    userAgent = createUserAgentString(userAgent, userAgentMarker);
                }
            }
        }
        // if custom user agent is set via ClientConfiguration, append it to the end
        if (!ClientConfiguration.DEFAULT_USER_AGENT.equals(config.getUserAgent())) {
            userAgent = createUserAgentString(userAgent, config.getUserAgent());
        }
        request.addHeader(HEADER_USER_AGENT, userAgent);
    }

    /**
     * Appends the given user-agent string to the existing one and returns it.
     */
    static String createUserAgentString(String existingUserAgentString, String userAgent) {
        if (existingUserAgentString.contains(userAgent)) {
            return existingUserAgentString;
        } else {
            return existingUserAgentString.trim() + " " + userAgent.trim();
        }
    }

    /**
     * Shuts down this HTTP client object, releasing any resources that might be
     * held open. This is an optional method, and callers are not expected to
     * call it, but can if they want to explicitly release any open resources.
     * Once a client has been shutdown, it cannot be used to make more requests.
     */
    public void shutdown() {
        httpClient.shutdown();
    }

    /**
     * Returns true if a failed request should be retried.
     *
     * @param originalRequest The original service request that is being
     *            executed.
     * @param inputStream The current HTTP method being executed.
     * @param exception The client/service exception from the failed request.
     * @param requestCount The number of times the current request has been
     *            attempted.
     * @return True if the failed request should be retried.
     */
    private boolean shouldRetry(AmazonWebServiceRequest originalRequest, InputStream inputStream,
            AmazonClientException exception, int requestCount, RetryPolicy retryPolicy) {
        final int retries = requestCount - 1;

        int maxErrorRetry = config.getMaxErrorRetry();
        // We should use the maxErrorRetry in
        // the RetryPolicy if either the user has not explicitly set it in
        // ClientConfiguration, or the RetryPolicy is configured to take
        // higher precedence.
        if (maxErrorRetry < 0 || !retryPolicy.isMaxErrorRetryInClientConfigHonored()) {
            maxErrorRetry = retryPolicy.getMaxErrorRetry();
        }

        // Immediately fails when it has exceeds the max retry count.
        if (retries >= maxErrorRetry)
            return false;

        // Never retry on requests containing non-repeatable entity
        if (inputStream != null && !inputStream.markSupported()) {
            if (log.isDebugEnabled()) {
                log.debug("Content not repeatable");
            }
            return false;
        }

        // Pass all the context information to the RetryCondition and let it
        // decide whether it should be retried.
        return retryPolicy.getRetryCondition().shouldRetry(originalRequest, exception, retries);
    }

    private static boolean isTemporaryRedirect(HttpResponse response) {
        int statusCode = response.getStatusCode();
        String location = response.getHeaders().get("Location");
        return statusCode == 307 && location != null && !location.isEmpty();
    }

    private boolean isRequestSuccessful(HttpResponse response) {
        int statusCode = response.getStatusCode();
        return statusCode >= 200 && statusCode < 300;
    }

    /**
     * Handles a successful response from a service call by unmarshalling the
     * results using the specified response handler.
     *
     * @param <T> The type of object expected in the response.
     * @param request The original request that generated the response being
     *            handled.
     * @param responseHandler The response unmarshaller used to interpret the
     *            contents of the response.
     * @param response The HTTP method that was invoked, and contains the contents
     *            of the response.
     * @param executionContext Extra state information about the request
     *            currently being executed.
     * @return The contents of the response, unmarshalled using the specified
     *         response handler.
     * @throws IOException If any problems were encountered reading the response
     *             contents from the HTTP method object.
     */
    <T> T handleResponse(Request<?> request, HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler,
            HttpResponse response, ExecutionContext executionContext) throws IOException {
        try {
            AWSRequestMetrics awsRequestMetrics = executionContext.getAwsRequestMetrics();
            AmazonWebServiceResponse<? extends T> awsResponse;
            awsRequestMetrics.startEvent(AWSRequestMetrics.Field.ResponseProcessingTime);
            try {
                awsResponse = responseHandler.handle(response);
            } finally {
                awsRequestMetrics.endEvent(AWSRequestMetrics.Field.ResponseProcessingTime);
            }

            if (awsResponse == null)
                throw new RuntimeException("Unable to unmarshall response metadata. Response Code: "
                        + response.getStatusCode() + ", Response Text: " + response.getStatusText());

            if (requestLog.isDebugEnabled()) {
                requestLog.debug("Received successful response: " + response.getStatusCode() + ", AWS Request ID: "
                        + awsResponse.getRequestId());
            }
            awsRequestMetrics.addProperty(AWSRequestMetrics.Field.AWSRequestID, awsResponse.getRequestId());

            return awsResponse.getResult();
        } catch (CRC32MismatchException e) {
            throw e;
        } catch (IOException e) {
            throw e;
        } catch (Exception e) {
            String errorMessage = "Unable to unmarshall response (" + e.getMessage() + "). Response Code: "
                    + response.getStatusCode() + ", Response Text: " + response.getStatusText();
            throw new AmazonClientException(errorMessage, e);
        }
    }

    /**
     * Responsible for handling an error response, including unmarshalling the
     * error response into the most specific exception type possible, and
     * throwing the exception.
     *
     * @param request The request that generated the error response being
     *            handled.
     * @param errorResponseHandler The response handler responsible for
     *            unmarshalling the error response.
     * @param response The HTTP method containing the actual response content.
     * @throws IOException If any problems are encountering reading the error
     *             response.
     */
    AmazonServiceException handleErrorResponse(Request<?> request,
            HttpResponseHandler<AmazonServiceException> errorResponseHandler, HttpResponse response)
            throws IOException {
        int status = response.getStatusCode();

        AmazonServiceException exception = null;
        try {
            exception = errorResponseHandler.handle(response);
            requestLog.debug("Received error response: " + exception.toString());
        } catch (Exception e) {
            // If the errorResponseHandler doesn't work, then check for error
            // responses that don't have any content
            if (status == 413) {
                exception = new AmazonServiceException("Request entity too large");
                exception.setServiceName(request.getServiceName());
                exception.setStatusCode(413);
                exception.setErrorType(AmazonServiceException.ErrorType.Client);
                exception.setErrorCode("Request entity too large");
            } else if (status == 503 && "Service Unavailable".equalsIgnoreCase(response.getStatusText())) {
                exception = new AmazonServiceException("Service unavailable");
                exception.setServiceName(request.getServiceName());
                exception.setStatusCode(503);
                exception.setErrorType(AmazonServiceException.ErrorType.Service);
                exception.setErrorCode("Service unavailable");
            } else if (e instanceof IOException) {
                throw (IOException) e;
            } else {
                String errorMessage = "Unable to unmarshall error response (" + e.getMessage()
                        + "). Response Code: " + status + ", Response Text: " + response.getStatusText();
                throw new AmazonClientException(errorMessage, e);
            }
        }

        exception.setStatusCode(status);
        exception.setServiceName(request.getServiceName());
        exception.fillInStackTrace();
        return exception;
    }

    /**
     * Sleep for a period of time on failed request to avoid flooding a service
     * with retries.
     *
     * @param originalRequest The original service request that is being
     *            executed.
     * @param previousException Exception information for the previous attempt,
     *            if any.
     * @param requestCount current request count (including the next attempt
     *            after the delay)
     * @param retryPolicy The retry policy configured in this http client.
     */
    private long pauseBeforeNextRetry(AmazonWebServiceRequest originalRequest,
            AmazonClientException previousException, int requestCount, RetryPolicy retryPolicy) {
        final int retries = requestCount // including next attempt
                - 1 // number of attempted requests
                - 1; // number of attempted retries

        long delay = retryPolicy.getBackoffStrategy().delayBeforeNextRetry(originalRequest, previousException,
                retries);

        if (log.isDebugEnabled()) {
            log.debug("Retriable error detected, " + "will retry in " + delay + "ms, attempt number: " + retries);
        }

        try {
            Thread.sleep(delay);
            return delay;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new AmazonClientException(e.getMessage(), e);
        }
    }

    /**
     * Returns date string from the exception message body in form of
     * yyyyMMdd'T'HHmmss'Z' We needed to extract date from the message body
     * because SQS is the only service that does not provide date header in the
     * response. Example, when device time is behind than the server time than
     * we get a string that looks something like this:
     * "Signature expired: 20130401T030113Z is now earlier than 20130401T034613Z (20130401T040113Z - 15 min.)"
     *
     * @param body The message from where the server time is being extracted
     * @return Return datetime in string format (yyyyMMdd'T'HHmmss'Z')
     */
    private String getServerDateFromException(String body) {
        int startPos = body.indexOf("(");
        int endPos = 0;
        if (body.contains(" + 15")) {
            endPos = body.indexOf(" + 15");
        } else {
            endPos = body.indexOf(" - 15");
        }
        String msg = body.substring(startPos + 1, endPos);
        return msg;
    }

    int parseClockSkewOffset(HttpResponse response, AmazonServiceException exception) {
        Date deviceDate = new Date();
        Date serverDate = null;
        String serverDateStr = null;
        String responseDateHeader = response.getHeaders().get("Date");

        try {

            if (responseDateHeader == null || responseDateHeader.isEmpty()) {
                // SQS doesn't return Date header
                serverDateStr = getServerDateFromException(exception.getMessage());
                serverDate = DateUtils.parseCompressedISO8601Date(serverDateStr);
            } else {
                serverDateStr = responseDateHeader;
                serverDate = DateUtils.parseRFC822Date(serverDateStr);
            }
        } catch (RuntimeException e) {
            log.warn("Unable to parse clock skew offset from response: " + serverDateStr, e);
            return 0;
        }

        long diff = deviceDate.getTime() - serverDate.getTime();
        return (int) (diff / 1000);
    }

    @Override
    protected void finalize() throws Throwable {
        this.shutdown();
        super.finalize();
    }

    /**
     * Returns the http client specific request metric collector; or null if
     * there is none.
     */
    public RequestMetricCollector getRequestMetricCollector() {
        return requestMetricCollector;
    }
}