blob: f39ab2a8f66a2084b24bdda6d404f68ef6386158 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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 org.jclouds.http.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.propagate;
import static org.jclouds.Constants.PROPERTY_IDEMPOTENT_METHODS;
import static org.jclouds.http.HttpUtils.checkRequestHasContentLengthOrChunkedEncoding;
import static org.jclouds.http.HttpUtils.releasePayload;
import static org.jclouds.http.HttpUtils.wirePayloadIfEnabled;
import static org.jclouds.util.Throwables2.getFirstThrowableOfType;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Set;
import javax.annotation.Resource;
import javax.inject.Named;
import org.jclouds.Constants;
import org.jclouds.http.HttpCommand;
import org.jclouds.http.HttpCommandExecutorService;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpRequestFilter;
import org.jclouds.http.HttpResponse;
import org.jclouds.http.HttpResponseException;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.IOExceptionRetryHandler;
import org.jclouds.http.handlers.DelegatingErrorHandler;
import org.jclouds.http.handlers.DelegatingRetryHandler;
import org.jclouds.io.ContentMetadataCodec;
import org.jclouds.logging.Logger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
public abstract class BaseHttpCommandExecutorService<Q> implements HttpCommandExecutorService {
protected final HttpUtils utils;
protected final ContentMetadataCodec contentMetadataCodec;
protected final DelegatingRetryHandler retryHandler;
protected final IOExceptionRetryHandler ioRetryHandler;
protected final DelegatingErrorHandler errorHandler;
@Resource
protected Logger logger = Logger.NULL;
@Resource
@Named(Constants.LOGGER_HTTP_HEADERS)
protected Logger headerLog = Logger.NULL;
protected final HttpWire wire;
private final Set<String> idempotentMethods;
protected BaseHttpCommandExecutorService(HttpUtils utils, ContentMetadataCodec contentMetadataCodec,
DelegatingRetryHandler retryHandler, IOExceptionRetryHandler ioRetryHandler,
DelegatingErrorHandler errorHandler, HttpWire wire,
@Named(PROPERTY_IDEMPOTENT_METHODS) String idempotentMethods) {
this.utils = checkNotNull(utils, "utils");
this.contentMetadataCodec = checkNotNull(contentMetadataCodec, "contentMetadataCodec");
this.retryHandler = checkNotNull(retryHandler, "retryHandler");
this.ioRetryHandler = checkNotNull(ioRetryHandler, "ioRetryHandler");
this.errorHandler = checkNotNull(errorHandler, "errorHandler");
this.wire = checkNotNull(wire, "wire");
this.idempotentMethods = ImmutableSet.copyOf(idempotentMethods.split(","));
}
@Override
public HttpResponse invoke(HttpCommand command) {
HttpResponse response = null;
for (;;) {
HttpRequest request = command.getCurrentRequest();
Q nativeRequest = null;
try {
for (HttpRequestFilter filter : request.getFilters()) {
request = filter.filter(request);
}
checkRequestHasContentLengthOrChunkedEncoding(request,
"After filtering, the request has neither chunked encoding nor content length: " + request);
logger.debug("Sending request %s: %s", request.hashCode(), request.getRequestLine());
wirePayloadIfEnabled(wire, request);
utils.logRequest(headerLog, request, ">>");
nativeRequest = convert(request);
response = invoke(nativeRequest);
logger.debug("Receiving response %s: %s", request.hashCode(), response.getStatusLine());
utils.logResponse(headerLog, response, "<<");
if (response.getPayload() != null && wire.enabled())
wire.input(response);
nativeRequest = null; // response took ownership of streams
int statusCode = response.getStatusCode();
if (statusCode >= 300) {
if (shouldContinue(command, response))
continue;
else
break;
} else {
break;
}
} catch (Exception e) {
IOException ioe = getFirstThrowableOfType(e, IOException.class);
if (ioe != null && shouldContinue(command, ioe)) {
continue;
}
command.setException(new HttpResponseException(e.getMessage() + " connecting to "
+ command.getCurrentRequest().getRequestLine(), command, null, e));
break;
} finally {
cleanup(nativeRequest);
}
}
if (command.getException() != null)
throw propagate(command.getException());
return response;
}
@VisibleForTesting
boolean shouldContinue(HttpCommand command, HttpResponse response) {
boolean shouldContinue = false;
if (retryHandler.shouldRetryRequest(command, response)) {
shouldContinue = true;
} else {
errorHandler.handleError(command, response);
}
// At this point we are going to send a new request or we have just handled the error, so
// we should make sure that any open stream is closed.
releasePayload(response);
return shouldContinue;
}
boolean shouldContinue(HttpCommand command, IOException response) {
// Even though Java does not want to handle it this way,
// treat a Protocol Exception on PUT with 100-Continue as a case of Unauthorized (and attempt to retry)
if (command.getCurrentRequest().getMethod().equals("PUT")
&& command.getCurrentRequest().getHeaders().containsEntry("Expect", "100-continue")
&& response instanceof ProtocolException
&& response.getMessage().equals("Server rejected operation")
) {
logger.debug("Caught a protocol exception on a 100-continue PUT request. Attempting to retry.");
return isIdempotent(command) && retryHandler.shouldRetryRequest(command, HttpResponse.builder().statusCode(401).message("Unauthorized").build());
}
return isIdempotent(command) && ioRetryHandler.shouldRetryRequest(command, response);
}
private boolean isIdempotent(HttpCommand command) {
String method = command.getCurrentRequest().getMethod();
if (!idempotentMethods.contains(method)) {
logger.error("Command not considered safe to retry because request method is %1$s: %2$s", method, command);
return false;
} else {
return true;
}
}
protected abstract Q convert(HttpRequest request) throws IOException, InterruptedException;
protected abstract HttpResponse invoke(Q nativeRequest) throws IOException, InterruptedException;
protected abstract void cleanup(Q nativeRequest);
}