/*
* Copyright 2010 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 com.amazonaws.http;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Map.Entry;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.NoHttpResponseException;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.AmazonWebServiceResponse;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.ResponseMetadata;
import com.amazonaws.util.CountingInputStream;
import com.amazonaws.util.HttpUtils;
import com.amazonaws.util.ResponseMetadataCache;
import com.amazonaws.util.VersionInfoUtils;
public class HttpClient {
/**
* 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).
*/
private static final Log log = LogFactory.getLog(HttpClient.class);
private static final Log unmarshallerPerformanceLog = LogFactory.getLog("com.amazonaws.unmarshaller.performance");
/** Internal client for sending HTTP requests */
private org.apache.commons.httpclient.HttpClient httpClient;
private static final String DEFAULT_ENCODING = "UTF-8";
/** Maximum exponential back-off time before retrying a request */
private static final int MAX_BACKOFF_IN_MILLISECONDS = 30 * 1000;
/** Client configuration options, such as proxy settings, max retries, etc. */
private final ClientConfiguration config;
/** Cache of metadata for recently executed requests for diagnostic purposes */
private ResponseMetadataCache responseMetadataCache = new ResponseMetadataCache(50);
/**
* Constructs a new AWS client using the specified client configuration
* options (ex: max retry attempts, proxy settings, etc).
*
* @param clientConfiguration
* Configuration options specifying how this client will
* communicate with AWS (ex: proxy settings, retry count, etc.).
*/
public HttpClient(ClientConfiguration clientConfiguration) {
this.config = clientConfiguration;
configureHttpClient();
}
/**
* 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.
*/
public ResponseMetadata getResponseMetadataForRequest(AmazonWebServiceRequest request) {
return responseMetadataCache.get(request);
}
public <T> T execute(HttpRequest request,
HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler,
HttpResponseHandler<AmazonServiceException> errorResponseHandler)
throws AmazonServiceException {
URI endpoint = request.getEndpoint();
HttpMethodBase method = createHttpMethodFromRequest(request);
/* Set content type and encoding */
if (method.getRequestHeader("Content-Type") == null) {
log.debug("Setting content-type to application/x-www-form-urlencoded; " +
"charset=" + DEFAULT_ENCODING.toLowerCase());
method.addRequestHeader("Content-Type",
"application/x-www-form-urlencoded; " +
"charset=" + DEFAULT_ENCODING.toLowerCase());
} else {
log.debug("Not overwriting Content-Type; already set to: " + method.getRequestHeader("Content-Type"));
}
/*
* 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;
/*
* Apache HttpClient omits the port number in the Host header (even if
* we explicitly specify it) if it's the default port for the protocol
* in use. To ensure that we use the same Host header in the request and
* in the calculated string to sign (even if Apache HttpClient changed
* and started honoring our explicit host with endpoint), we follow this
* same behavior here and in the QueryString signer.
*/
String hostHeader = endpoint.getHost();
if (HttpUtils.isUsingNonDefaultPort(endpoint)) {
hostHeader += ":" + endpoint.getPort();
}
method.addRequestHeader("Host", hostHeader);
// When we release connections, the connection manager leaves them
// open so they can be reused. We want to close out any idle
// connections so that they don't sit around in CLOSE_WAIT.
httpClient.getHttpConnectionManager().closeIdleConnections(1000 * 30);
int retries = 0;
while (true) {
try {
requestLog.info("Sending Request: " + request.toString());
retries++;
int status = httpClient.executeMethod(method);
if (isRequestSuccessful(status)) {
/*
* If we get back any 2xx status code, then we know we should
* treat the service call as successful.
*/
leaveHttpConnectionOpen = responseHandler.needsConnectionLeftOpen();
return handleResponse(request, responseHandler, method);
} else if (isTemporaryRedirect(method, status)) {
/*
* 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.
*/
Header locationHeader = method.getResponseHeader("location");
String redirectedLocation = locationHeader.getValue();
log.debug("Redirecting to: " + redirectedLocation);
method.setURI(new org.apache.commons.httpclient.URI(redirectedLocation, false));
} else {
leaveHttpConnectionOpen = errorResponseHandler.needsConnectionLeftOpen();
AmazonServiceException exception = handleErrorResponse(request, errorResponseHandler, method);
if (shouldRetry(exception, retries)) {
requestLog.info("Received retryable error response: " + status + ", retrying request...");
pauseExponentially(retries);
} else {
throw exception;
}
}
} catch (IOException ioe) {
log.error("Unable to execute HTTP request: " + ioe.getMessage());
throw new AmazonClientException("Unable to execute HTTP request: "
+ ioe.getMessage(), ioe);
} 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) {
try {method.getResponseBodyAsStream().close();} catch (Throwable t) {}
method.releaseConnection();
}
}
}
}
/**
* 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() {
HttpConnectionManager connectionManager = httpClient.getHttpConnectionManager();
if (connectionManager instanceof MultiThreadedHttpConnectionManager) {
((MultiThreadedHttpConnectionManager)connectionManager).shutdown();
}
}
/**
* Returns true if a failed request should be retried.
*
* @param exception
* The exception from the failed request.
* @param retries
* The number of times the current request has been attempted.
*
* @return True if the failed request should be retried.
*/
private boolean shouldRetry(AmazonServiceException exception, int retries) {
if (retries > config.getMaxErrorRetry()) {
return false;
}
/*
* For 500 internal server errors and 503 service
* unavailable errors, we want to retry, but we need to use
* an exponential back-off strategy so that we don't overload
* a server with a flood of retries. If we've surpassed our
* retry limit we handle the error response as a non-retryable
* error and go ahead and throw it back to the user as an exception.
*/
int statusCode = exception.getStatusCode();
if (statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR ||
statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE) {
return true;
}
/*
* Throttling is reported as a 400 error from newer services. To try and
* smooth out an occasional throttling error, we'll pause and retry,
* hoping that the pause is long enough for the request to get through
* the next time.
*/
if (statusCode == HttpStatus.SC_BAD_REQUEST &&
exception.getErrorCode().equals("Throttling")) {
return true;
}
return false;
}
private boolean isTemporaryRedirect(HttpMethodBase method, int status) {
return status == HttpStatus.SC_TEMPORARY_REDIRECT &&
method.getResponseHeader("location") != null;
}
private boolean isRequestSuccessful(int status) {
return status / 100 == HttpStatus.SC_OK / 100;
}
/**
* Creates an HttpClient method object based on the specified request and
* populates any parameters, headers, etc. from the original request.
*
* @param request
* The request to convert to an HttpClient method object.
*
* @return The converted HttpClient method object with any parameters,
* headers, etc. from the original request set.
*/
private HttpMethodBase createHttpMethodFromRequest(HttpRequest request) {
URI endpoint = request.getEndpoint();
String uri = endpoint.toString();
if (request.getResourcePath() != null && request.getResourcePath().length() > 0) {
if (request.getResourcePath().startsWith("/") == false) {
uri += "/";
}
uri += request.getResourcePath();
}
NameValuePair[] nameValuePairs = null;
if (request.getParameters().size() > 0) {
nameValuePairs = new NameValuePair[request.getParameters().size()];
int i = 0;
for (Entry<String, String> entry : request.getParameters().entrySet()) {
nameValuePairs[i++] = new NameValuePair(entry.getKey(), entry.getValue());
}
}
HttpMethodBase method;
if (request.getMethodName() == HttpMethodName.POST) {
PostMethod postMethod = new PostMethod(uri);
if (nameValuePairs != null) postMethod.addParameters(nameValuePairs);
method = postMethod;
} else if (request.getMethodName() == HttpMethodName.GET) {
GetMethod getMethod = new GetMethod(uri);
if (nameValuePairs != null) getMethod.setQueryString(nameValuePairs);
method = getMethod;
} else if (request.getMethodName() == HttpMethodName.PUT) {
PutMethod putMethod = new PutMethod(uri);
if (nameValuePairs != null) putMethod.setQueryString(nameValuePairs);
method = putMethod;
/*
* Enable 100-continue support for PUT operations, since this is
* where we're potentially uploading large amounts of data and want
* to find out as early as possible if an operation will fail. We
* don't want to do this for all operations since it will cause
* extra latency in the network interaction.
*/
putMethod.getParams().setBooleanParameter(HttpClientParams.USE_EXPECT_CONTINUE, true);
if (request.getContent() != null) {
putMethod.setRequestEntity(new RepeatableInputStreamRequestEntity(request));
}
} else if (request.getMethodName() == HttpMethodName.DELETE) {
DeleteMethod deleteMethod = new DeleteMethod(uri);
if (nameValuePairs != null) deleteMethod.setQueryString(nameValuePairs);
method = deleteMethod;
} else if (request.getMethodName() == HttpMethodName.HEAD) {
HeadMethod headMethod = new HeadMethod(uri);
if (nameValuePairs != null) headMethod.setQueryString(nameValuePairs);
method = headMethod;
} else {
throw new AmazonClientException("Unknown HTTP method name: " + request.getMethodName());
}
// No matter what type of HTTP method we're creating, we need to copy
// all the headers from the request.
for (Entry<String, String> entry : request.getHeaders().entrySet()) {
method.addRequestHeader(entry.getKey(), entry.getValue());
}
return method;
}
/**
* 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 method
* The HTTP method that was invoked, and contains the contents of
* the response.
*
* @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.
*/
private <T> T handleResponse(HttpRequest request,
HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler, HttpMethodBase method)
throws IOException {
HttpResponse httpResponse = createResponse(method, request);
if (responseHandler.needsConnectionLeftOpen()) {
httpResponse.setContent(new HttpMethodReleaseInputStream(method));
}
try {
CountingInputStream countingInputStream = null;
if (unmarshallerPerformanceLog.isTraceEnabled()) {
countingInputStream = new CountingInputStream(httpResponse.getContent());
httpResponse.setContent(countingInputStream);
}
long startTime = System.currentTimeMillis();
AmazonWebServiceResponse<? extends T> awsResponse = responseHandler.handle(httpResponse);
long endTime = System.currentTimeMillis();
if (unmarshallerPerformanceLog.isTraceEnabled()) {
unmarshallerPerformanceLog.trace(
countingInputStream.getByteCount() + ", " + (endTime - startTime));
}
if (awsResponse == null)
throw new RuntimeException("Unable to unmarshall response metadata");
responseMetadataCache.add(request.getOriginalRequest(), awsResponse.getResponseMetadata());
requestLog.info("Received successful response: " + method.getStatusCode()
+ ", AWS Request ID: " + awsResponse.getRequestId());
return awsResponse.getResult();
} catch (Exception e) {
String errorMessage = "Unable to unmarshall response (" + e.getMessage() + "): "
+ method.getResponseBodyAsString();
log.error(errorMessage, e);
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 method
* The HTTP method containing the actual response content.
*
* @throws IOException
* If any problems are encountering reading the error response.
*/
private AmazonServiceException handleErrorResponse(HttpRequest request,
HttpResponseHandler<AmazonServiceException> errorResponseHandler,
HttpMethodBase method) throws IOException {
int status = method.getStatusCode();
HttpResponse response = createResponse(method, request);
if (errorResponseHandler.needsConnectionLeftOpen()) {
response.setContent(new HttpMethodReleaseInputStream(method));
}
AmazonServiceException exception = null;
try {
exception = errorResponseHandler.handle(response);
requestLog.info("Received error response: " + exception.toString());
} catch (Exception e) {
String errorMessage = "Unable to unmarshall error response (" + e.getMessage() + "): "
+ method.getResponseBodyAsString();
log.error(errorMessage, e);
throw new AmazonClientException(errorMessage, e);
}
exception.setStatusCode(status);
exception.setServiceName(request.getServiceName());
exception.fillInStackTrace();
return exception;
}
/**
* Creates and initializes an HttpResponse object suitable to be passed to
* an HTTP response handler object.
*
* @param method
* The HTTP method that was invoked to get the response.
* @param request
* The HTTP request associated with the response.
*
* @return The new, initialized HttpResponse object ready to be passed to an
* HTTP response handler object.
*
* @throws IOException
* If there were any problems getting any response information
* from the HttpClient method object.
*/
private HttpResponse createResponse(HttpMethodBase method, HttpRequest request) throws IOException {
HttpResponse httpResponse = new HttpResponse(request);
httpResponse.setContent(method.getResponseBodyAsStream());
httpResponse.setStatusCode(method.getStatusCode());
httpResponse.setStatusText(method.getStatusText());
for (Header header : method.getResponseHeaders()) {
httpResponse.addHeader(header.getName(), header.getValue());
}
return httpResponse;
}
/**
* Exponential sleep on failed request to avoid flooding a service with
* retries.
*
* @param retries
* Current retry count.
*/
private void pauseExponentially(int retries) {
long delay = (long) (Math.pow(4, retries) * 100L);
delay = Math.min(delay, MAX_BACKOFF_IN_MILLISECONDS);
log.debug("Retriable error detected, will retry in " + delay + "ms, attempt number: " + retries);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
// Nothing we need to do here
}
}
/**
* Configure HttpClient with set of defaults as well as configuration.
*/
private void configureHttpClient() {
/* Form User-Agent information */
String userAgent = config.getUserAgent();
if (!(userAgent.equals(ClientConfiguration.DEFAULT_USER_AGENT))) {
userAgent += ", " + ClientConfiguration.DEFAULT_USER_AGENT;
}
userAgent += "-" + VersionInfoUtils.getVersion();
/* Set HTTP client parameters */
HttpClientParams httpClientParams = new HttpClientParams();
httpClientParams.setParameter(HttpMethodParams.USER_AGENT, userAgent);
httpClientParams.setParameter(HttpClientParams.RETRY_HANDLER, new RetryHandler());
/* Set host configuration */
HostConfiguration hostConfiguration = new HostConfiguration();
/* Set connection manager parameters */
HttpConnectionManagerParams connectionManagerParams = new HttpConnectionManagerParams();
connectionManagerParams.setConnectionTimeout(config.getConnectionTimeout());
connectionManagerParams.setSoTimeout(config.getSocketTimeout());
connectionManagerParams.setStaleCheckingEnabled(true);
connectionManagerParams.setTcpNoDelay(true);
connectionManagerParams.setMaxTotalConnections(config.getMaxConnections());
connectionManagerParams.setMaxConnectionsPerHost(hostConfiguration, config.getMaxConnections());
int socketSendBufferSizeHint = config.getSocketBufferSizeHints()[0];
if (socketSendBufferSizeHint > 0) {
connectionManagerParams.setSendBufferSize(socketSendBufferSizeHint);
}
int socketReceiveBufferSizeHint = config.getSocketBufferSizeHints()[1];
if (socketReceiveBufferSizeHint > 0) {
connectionManagerParams.setReceiveBufferSize(socketReceiveBufferSizeHint);
}
/* Set connection manager */
MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
connectionManager.setParams(connectionManagerParams);
httpClient = new org.apache.commons.httpclient.HttpClient(
httpClientParams, connectionManager);
/* Set proxy if configured */
String proxyHost = config.getProxyHost();
int proxyPort = config.getProxyPort();
if (proxyHost != null && proxyPort > 0) {
log.info("Configuring Proxy. Proxy Host: " + proxyHost + " "
+ "Proxy Port: " + proxyPort);
hostConfiguration.setProxy(proxyHost, proxyPort);
String proxyUsername = config.getProxyUsername();
String proxyPassword = config.getProxyPassword();
if (proxyUsername != null && proxyPassword != null) {
AuthScope authScope = new AuthScope(proxyHost, proxyPort);
UsernamePasswordCredentials credentials =
new UsernamePasswordCredentials(proxyUsername, proxyPassword);
httpClient.getState().setProxyCredentials(authScope, credentials);
}
}
httpClient.setHostConfiguration(hostConfiguration);
}
/**
* Implementation of HttpMethodRetryHandler that determines if a specific
* type of failure should be retried or not.
*/
private final class RetryHandler implements HttpMethodRetryHandler {
public boolean retryMethod(HttpMethod method,
IOException exception, int executionCount) {
if (executionCount > 3) {
log.debug("Maximum Number of Retry attempts reached, will not retry");
return false;
}
log.debug("Retrying request. Attempt " + executionCount);
if (exception instanceof NoHttpResponseException) {
log.debug("Retrying on NoHttpResponseException");
return true;
}
if (exception instanceof InterruptedIOException) {
log.debug("Will not retry on InterruptedIOException", exception);
return false;
}
if (exception instanceof UnknownHostException) {
log.debug("Will not retry on UnknownHostException", exception);
return false;
}
if (!method.isRequestSent()) {
log.debug("Retrying on failed sent request");
return true;
}
return false;
}
}
}