com.rt.storage.api.client.http.HttpRequest Maven / Gradle / Ivy
package com.rt.storage.api.client.http;
import com.rt.storage.api.client.util.Beta;
import com.rt.storage.api.client.util.LoggingStreamingContent;
import com.rt.storage.api.client.util.ObjectParser;
import com.rt.storage.api.client.util.Preconditions;
import com.rt.storage.api.client.util.Sleeper;
import com.rt.storage.api.client.util.StreamingContent;
import com.rt.storage.api.client.util.StringUtils;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.opencensus.common.Scope;
import io.opencensus.contrib.http.util.HttpTraceAttributeConstants;
import io.opencensus.trace.AttributeValue;
import io.opencensus.trace.Span;
import io.opencensus.trace.Tracer;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* HTTP request.
*
* Implementation is not thread-safe.
*
* @since 1.0
*/
public final class HttpRequest {
/**
* Current version of the API Client Library for Java.
*
* @since 1.8
*/
public static final String VERSION = getVersion();
/**
* User agent suffix for all requests.
*
*
Includes a {@code "(gzip)"} suffix in case the server -- as servers may do --
* checks the {@code User-Agent} header to try to detect if the client accepts gzip-encoded
* responses.
*
* @since 1.4
*/
public static final String USER_AGENT_SUFFIX = "Google-HTTP-Java-Client/" + VERSION + " (gzip)";
/**
* The default number of retries that will be allowed to execute before the request will be
* terminated.
*
* @see #getNumberOfRetries
* @since 1.22
*/
public static final int DEFAULT_NUMBER_OF_RETRIES = 10;
/**
* HTTP request execute interceptor to intercept the start of {@link #execute()} (before executing
* the HTTP request) or {@code null} for none.
*/
private HttpExecuteInterceptor executeInterceptor;
/** HTTP request headers. */
private HttpHeaders headers = new HttpHeaders();
/**
* HTTP response headers.
*
*
For example, this can be used if you want to use a subclass of {@link HttpHeaders} called
* MyHeaders to process the response:
*
*
* static String executeAndGetValueOfSomeCustomHeader(HttpRequest request) {
* MyHeaders responseHeaders = new MyHeaders();
* request.responseHeaders = responseHeaders;
* HttpResponse response = request.execute();
* return responseHeaders.someCustomHeader;
* }
*
*/
private HttpHeaders responseHeaders = new HttpHeaders();
/**
* The number of retries that will be allowed to execute before the request will be terminated or
* {@code 0} to not retry requests. Retries occur as a result of either {@link
* HttpUnsuccessfulResponseHandler} or {@link HttpIOExceptionHandler} which handles abnormal HTTP
* response or the I/O exception.
*/
private int numRetries = DEFAULT_NUMBER_OF_RETRIES;
/**
* Determines the limit to the content size that will be logged during {@link #execute()}.
*
* Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
*
If the content size is greater than this limit then it will not be logged.
*
*
Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
*
Defaults to 16KB.
*/
private int contentLoggingLimit = 0x4000;
/** Determines whether logging should be enabled for this request. Defaults to {@code true}. */
private boolean loggingEnabled = true;
/** Determines whether logging in form of curl commands should be enabled for this request. */
private boolean curlLoggingEnabled = true;
/** HTTP request content or {@code null} for none. */
private HttpContent content;
/** HTTP transport. */
private final HttpTransport transport;
/** HTTP request method or {@code null} for none. */
private String requestMethod;
/** HTTP request URL. */
private GenericUrl url;
/** Timeout in milliseconds to establish a connection or {@code 0} for an infinite timeout. */
private int connectTimeout = 20 * 1000;
/**
* Timeout in milliseconds to read data from an established connection or {@code 0} for an
* infinite timeout.
*/
private int readTimeout = 20 * 1000;
/** Timeout in milliseconds to set POST/PUT data or {@code 0} for an infinite timeout. */
private int writeTimeout = 0;
/** HTTP unsuccessful (non-2XX) response handler or {@code null} for none. */
private HttpUnsuccessfulResponseHandler unsuccessfulResponseHandler;
/** HTTP I/O exception handler or {@code null} for none. */
@Beta private HttpIOExceptionHandler ioExceptionHandler;
/** HTTP response interceptor or {@code null} for none. */
private HttpResponseInterceptor responseInterceptor;
/** Parser used to parse responses. */
private ObjectParser objectParser;
/** HTTP content encoding or {@code null} for none. */
private HttpEncoding encoding;
/** The {@link BackOffPolicy} to use between retry attempts or {@code null} for none. */
@Deprecated @Beta private BackOffPolicy backOffPolicy;
/** Whether to automatically follow redirects ({@code true} by default). */
private boolean followRedirects = true;
/** Whether to use raw redirect URLs ({@code false} by default). */
private boolean useRawRedirectUrls = false;
/**
* Whether to throw an exception at the end of {@link #execute()} on an HTTP error code (non-2XX)
* after all retries and response handlers have been exhausted ({@code true} by default).
*/
private boolean throwExceptionOnExecuteError = true;
/**
* Whether to retry the request if an {@link IOException} is encountered in {@link
* LowLevelHttpRequest#execute()}.
*/
@Deprecated @Beta private boolean retryOnExecuteIOException = false;
/**
* Whether to not add the suffix {@link #USER_AGENT_SUFFIX} to the User-Agent header.
*
*
It is {@code false} by default.
*/
private boolean suppressUserAgentSuffix;
/** Sleeper. */
private Sleeper sleeper = Sleeper.DEFAULT;
/** OpenCensus tracing component. */
private final Tracer tracer = OpenCensusUtils.getTracer();
/**
* Determines whether {@link HttpResponse#getContent()} of this request should return raw input
* stream or not.
*
*
It is {@code false} by default.
*/
private boolean responseReturnRawInputStream = false;
/**
* @param transport HTTP transport
* @param requestMethod HTTP request method or {@code null} for none
*/
HttpRequest(HttpTransport transport, String requestMethod) {
this.transport = transport;
setRequestMethod(requestMethod);
}
/**
* Returns the HTTP transport.
*
* @since 1.5
*/
public HttpTransport getTransport() {
return transport;
}
/**
* Returns the HTTP request method or {@code null} for none.
*
* @since 1.12
*/
public String getRequestMethod() {
return requestMethod;
}
/**
* Sets the HTTP request method or {@code null} for none.
*
* @since 1.12
*/
public HttpRequest setRequestMethod(String requestMethod) {
Preconditions.checkArgument(requestMethod == null || HttpMediaType.matchesToken(requestMethod));
this.requestMethod = requestMethod;
return this;
}
/**
* Returns the HTTP request URL.
*
* @since 1.5
*/
public GenericUrl getUrl() {
return url;
}
/**
* Sets the HTTP request URL.
*
* @since 1.5
*/
public HttpRequest setUrl(GenericUrl url) {
this.url = Preconditions.checkNotNull(url);
return this;
}
/**
* Returns the HTTP request content or {@code null} for none.
*
* @since 1.5
*/
public HttpContent getContent() {
return content;
}
/**
* Sets the HTTP request content or {@code null} for none.
*
* @since 1.5
*/
public HttpRequest setContent(HttpContent content) {
this.content = content;
return this;
}
/**
* Returns the HTTP content encoding or {@code null} for none.
*
* @since 1.14
*/
public HttpEncoding getEncoding() {
return encoding;
}
/**
* Sets the HTTP content encoding or {@code null} for none.
*
* @since 1.14
*/
public HttpRequest setEncoding(HttpEncoding encoding) {
this.encoding = encoding;
return this;
}
/**
* {@link Beta}
* Returns the {@link BackOffPolicy} to use between retry attempts or {@code null} for none.
*
* @since 1.7
* @deprecated (scheduled to be removed in 1.18). {@link
* #setUnsuccessfulResponseHandler(HttpUnsuccessfulResponseHandler)} with a new {@link
* HttpBackOffUnsuccessfulResponseHandler} instead.
*/
@Deprecated
@Beta
public BackOffPolicy getBackOffPolicy() {
return backOffPolicy;
}
/**
* {@link Beta}
* Sets the {@link BackOffPolicy} to use between retry attempts or {@code null} for none.
*
* @since 1.7
* @deprecated (scheduled to be removed in 1.18). Use {@link
* #setUnsuccessfulResponseHandler(HttpUnsuccessfulResponseHandler)} with a new {@link
* HttpBackOffUnsuccessfulResponseHandler} instead.
*/
@Deprecated
@Beta
public HttpRequest setBackOffPolicy(BackOffPolicy backOffPolicy) {
this.backOffPolicy = backOffPolicy;
return this;
}
/**
* Returns the limit to the content size that will be logged during {@link #execute()}.
*
*
If the content size is greater than this limit then it will not be logged.
*
*
Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
*
Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
*
Defaults to 16KB.
*
* @since 1.7
*/
public int getContentLoggingLimit() {
return contentLoggingLimit;
}
/**
* Set the limit to the content size that will be logged during {@link #execute()}.
*
*
If the content size is greater than this limit then it will not be logged.
*
*
Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
*
Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
*
Defaults to 16KB.
*
* @since 1.7
*/
public HttpRequest setContentLoggingLimit(int contentLoggingLimit) {
Preconditions.checkArgument(
contentLoggingLimit >= 0, "The content logging limit must be non-negative.");
this.contentLoggingLimit = contentLoggingLimit;
return this;
}
/**
* Returns whether logging should be enabled for this request.
*
*
Defaults to {@code true}.
*
* @since 1.9
*/
public boolean isLoggingEnabled() {
return loggingEnabled;
}
/**
* Sets whether logging should be enabled for this request.
*
*
Defaults to {@code true}.
*
* @since 1.9
*/
public HttpRequest setLoggingEnabled(boolean loggingEnabled) {
this.loggingEnabled = loggingEnabled;
return this;
}
/**
* Returns whether logging in form of curl commands is enabled for this request.
*
* @since 1.11
*/
public boolean isCurlLoggingEnabled() {
return curlLoggingEnabled;
}
/**
* Sets whether logging in form of curl commands should be enabled for this request.
*
*
Defaults to {@code true}.
*
* @since 1.11
*/
public HttpRequest setCurlLoggingEnabled(boolean curlLoggingEnabled) {
this.curlLoggingEnabled = curlLoggingEnabled;
return this;
}
/**
* Returns the timeout in milliseconds to establish a connection or {@code 0} for an infinite
* timeout.
*
* @since 1.5
*/
public int getConnectTimeout() {
return connectTimeout;
}
/**
* Sets the timeout in milliseconds to establish a connection or {@code 0} for an infinite
* timeout.
*
*
By default it is 20000 (20 seconds).
*
* @since 1.5
*/
public HttpRequest setConnectTimeout(int connectTimeout) {
Preconditions.checkArgument(connectTimeout >= 0);
this.connectTimeout = connectTimeout;
return this;
}
/**
* Returns the timeout in milliseconds to read data from an established connection or {@code 0}
* for an infinite timeout.
*
*
By default it is 20000 (20 seconds).
*
* @since 1.5
*/
public int getReadTimeout() {
return readTimeout;
}
/**
* Sets the timeout in milliseconds to read data from an established connection or {@code 0} for
* an infinite timeout.
*
* @since 1.5
*/
public HttpRequest setReadTimeout(int readTimeout) {
Preconditions.checkArgument(readTimeout >= 0);
this.readTimeout = readTimeout;
return this;
}
/**
* Returns the timeout in milliseconds to send POST/PUT data or {@code 0} for an infinite timeout.
*
*
By default it is 0 (infinite).
*
* @since 1.27
*/
public int getWriteTimeout() {
return writeTimeout;
}
/**
* Sets the timeout in milliseconds to send POST/PUT data or {@code 0} for an infinite timeout.
*
* @since 1.27
*/
public HttpRequest setWriteTimeout(int writeTimeout) {
Preconditions.checkArgument(writeTimeout >= 0);
this.writeTimeout = writeTimeout;
return this;
}
/**
* Returns the HTTP request headers.
*
* @since 1.5
*/
public HttpHeaders getHeaders() {
return headers;
}
/**
* Sets the HTTP request headers.
*
*
By default, this is a new unmodified instance of {@link HttpHeaders}.
*
* @since 1.5
*/
public HttpRequest setHeaders(HttpHeaders headers) {
this.headers = Preconditions.checkNotNull(headers);
return this;
}
/**
* Returns the HTTP response headers.
*
* @since 1.5
*/
public HttpHeaders getResponseHeaders() {
return responseHeaders;
}
/**
* Sets the HTTP response headers.
*
*
By default, this is a new unmodified instance of {@link HttpHeaders}.
*
*
For example, this can be used if you want to use a subclass of {@link HttpHeaders} called
* MyHeaders to process the response:
*
*
* static String executeAndGetValueOfSomeCustomHeader(HttpRequest request) {
* MyHeaders responseHeaders = new MyHeaders();
* request.responseHeaders = responseHeaders;
* HttpResponse response = request.execute();
* return responseHeaders.someCustomHeader;
* }
*
*
* @since 1.5
*/
public HttpRequest setResponseHeaders(HttpHeaders responseHeaders) {
this.responseHeaders = Preconditions.checkNotNull(responseHeaders);
return this;
}
/**
* Returns the HTTP request execute interceptor to intercept the start of {@link #execute()}
* (before executing the HTTP request) or {@code null} for none.
*
* @since 1.5
*/
public HttpExecuteInterceptor getInterceptor() {
return executeInterceptor;
}
/**
* Sets the HTTP request execute interceptor to intercept the start of {@link #execute()} (before
* executing the HTTP request) or {@code null} for none.
*
* @since 1.5
*/
public HttpRequest setInterceptor(HttpExecuteInterceptor interceptor) {
this.executeInterceptor = interceptor;
return this;
}
/**
* Returns the HTTP unsuccessful (non-2XX) response handler or {@code null} for none.
*
* @since 1.5
*/
public HttpUnsuccessfulResponseHandler getUnsuccessfulResponseHandler() {
return unsuccessfulResponseHandler;
}
/**
* Sets the HTTP unsuccessful (non-2XX) response handler or {@code null} for none.
*
* @since 1.5
*/
public HttpRequest setUnsuccessfulResponseHandler(
HttpUnsuccessfulResponseHandler unsuccessfulResponseHandler) {
this.unsuccessfulResponseHandler = unsuccessfulResponseHandler;
return this;
}
/**
* {@link Beta}
* Returns the HTTP I/O exception handler or {@code null} for none.
*
* @since 1.15
*/
@Beta
public HttpIOExceptionHandler getIOExceptionHandler() {
return ioExceptionHandler;
}
/**
* {@link Beta}
* Sets the HTTP I/O exception handler or {@code null} for none.
*
* @since 1.15
*/
@Beta
public HttpRequest setIOExceptionHandler(HttpIOExceptionHandler ioExceptionHandler) {
this.ioExceptionHandler = ioExceptionHandler;
return this;
}
/**
* Returns the HTTP response interceptor or {@code null} for none.
*
* @since 1.13
*/
public HttpResponseInterceptor getResponseInterceptor() {
return responseInterceptor;
}
/**
* Sets the HTTP response interceptor or {@code null} for none.
*
* @since 1.13
*/
public HttpRequest setResponseInterceptor(HttpResponseInterceptor responseInterceptor) {
this.responseInterceptor = responseInterceptor;
return this;
}
/**
* Returns the number of retries that will be allowed to execute before the request will be
* terminated or {@code 0} to not retry requests. Retries occur as a result of either {@link
* HttpUnsuccessfulResponseHandler} or {@link HttpIOExceptionHandler} which handles abnormal HTTP
* response or the I/O exception.
*
* @since 1.5
*/
public int getNumberOfRetries() {
return numRetries;
}
/**
* Sets the number of retries that will be allowed to execute before the request will be
* terminated or {@code 0} to not retry requests. Retries occur as a result of either {@link
* HttpUnsuccessfulResponseHandler} or {@link HttpIOExceptionHandler} which handles abnormal HTTP
* response or the I/O exception.
*
* The default value is {@link #DEFAULT_NUMBER_OF_RETRIES}.
*
* @since 1.5
*/
public HttpRequest setNumberOfRetries(int numRetries) {
Preconditions.checkArgument(numRetries >= 0);
this.numRetries = numRetries;
return this;
}
/**
* Sets the {@link ObjectParser} used to parse the response to this request or {@code null} for
* none.
*
*
This parser will be preferred over any registered HttpParser.
*
* @since 1.10
*/
public HttpRequest setParser(ObjectParser parser) {
this.objectParser = parser;
return this;
}
/**
* Returns the {@link ObjectParser} used to parse the response or {@code null} for none.
*
* @since 1.10
*/
public final ObjectParser getParser() {
return objectParser;
}
/**
* Returns whether to follow redirects automatically.
*
* @since 1.6
*/
public boolean getFollowRedirects() {
return followRedirects;
}
/**
* Sets whether to follow redirects automatically.
*
*
The default value is {@code true}.
*
* @since 1.6
*/
public HttpRequest setFollowRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}
/** Return whether to use raw redirect URLs. */
public boolean getUseRawRedirectUrls() {
return useRawRedirectUrls;
}
/**
* Sets whether to use raw redirect URLs.
*
*
The default value is {@code false}.
*/
public HttpRequest setUseRawRedirectUrls(boolean useRawRedirectUrls) {
this.useRawRedirectUrls = useRawRedirectUrls;
return this;
}
/**
* Returns whether to throw an exception at the end of {@link #execute()} on an HTTP error code
* (non-2XX) after all retries and response handlers have been exhausted.
*
* @since 1.7
*/
public boolean getThrowExceptionOnExecuteError() {
return throwExceptionOnExecuteError;
}
/**
* Sets whether to throw an exception at the end of {@link #execute()} on a HTTP error code
* (non-2XX) after all retries and response handlers have been exhausted.
*
*
The default value is {@code true}.
*
* @since 1.7
*/
public HttpRequest setThrowExceptionOnExecuteError(boolean throwExceptionOnExecuteError) {
this.throwExceptionOnExecuteError = throwExceptionOnExecuteError;
return this;
}
/**
* {@link Beta}
* Returns whether to retry the request if an {@link IOException} is encountered in {@link
* LowLevelHttpRequest#execute()}.
*
* @since 1.9
* @deprecated (scheduled to be removed in 1.18) Use {@link
* #setIOExceptionHandler(HttpIOExceptionHandler)} instead.
*/
@Deprecated
@Beta
public boolean getRetryOnExecuteIOException() {
return retryOnExecuteIOException;
}
/**
* {@link Beta}
* Sets whether to retry the request if an {@link IOException} is encountered in {@link
* LowLevelHttpRequest#execute()}.
*
*
The default value is {@code false}.
*
* @since 1.9
* @deprecated (scheduled to be removed in 1.18) Use {@link
* #setIOExceptionHandler(HttpIOExceptionHandler)} instead.
*/
@Deprecated
@Beta
public HttpRequest setRetryOnExecuteIOException(boolean retryOnExecuteIOException) {
this.retryOnExecuteIOException = retryOnExecuteIOException;
return this;
}
/**
* Returns whether to not add the suffix {@link #USER_AGENT_SUFFIX} to the User-Agent header.
*
* @since 1.11
*/
public boolean getSuppressUserAgentSuffix() {
return suppressUserAgentSuffix;
}
/**
* Sets whether to not add the suffix {@link #USER_AGENT_SUFFIX} to the User-Agent header.
*
*
The default value is {@code false}.
*
* @since 1.11
*/
public HttpRequest setSuppressUserAgentSuffix(boolean suppressUserAgentSuffix) {
this.suppressUserAgentSuffix = suppressUserAgentSuffix;
return this;
}
/**
* Returns whether {@link HttpResponse#getContent()} should return raw input stream for this
* request.
*
* @since 1.29
*/
public boolean getResponseReturnRawInputStream() {
return responseReturnRawInputStream;
}
/**
* Sets whether {@link HttpResponse#getContent()} should return raw input stream for this request.
*
*
The default value is {@code false}.
*
* @since 1.29
*/
public HttpRequest setResponseReturnRawInputStream(boolean responseReturnRawInputStream) {
this.responseReturnRawInputStream = responseReturnRawInputStream;
return this;
}
/**
* Execute the HTTP request and returns the HTTP response.
*
*
Note that regardless of the returned status code, the HTTP response content has not been
* parsed yet, and must be parsed by the calling code.
*
*
Note that when calling to this method twice or more, the state of this HTTP request object
* isn't cleared, so the request will continue where it was left. For example, the state of the
* {@link HttpUnsuccessfulResponseHandler} attached to this HTTP request will remain the same as
* it was left after last execute.
*
*
Almost all details of the request and response are logged if {@link Level#CONFIG} is
* loggable. The only exception is the value of the {@code Authorization} header which is only
* logged if {@link Level#ALL} is loggable.
*
*
Callers should call {@link HttpResponse#disconnect} when the returned HTTP response object
* is no longer needed. However, {@link HttpResponse#disconnect} does not have to be called if the
* response stream is properly closed. Example usage:
*
*
* HttpResponse response = request.execute();
* try {
* // process the HTTP response object
* } finally {
* response.disconnect();
* }
*
*
* @return HTTP response for an HTTP success response (or HTTP error response if {@link
* #getThrowExceptionOnExecuteError()} is {@code false})
* @throws HttpResponseException for an HTTP error response (only if {@link
* #getThrowExceptionOnExecuteError()} is {@code true})
* @see HttpResponse#isSuccessStatusCode()
*/
@SuppressWarnings("deprecation")
public HttpResponse execute() throws IOException {
boolean retryRequest = false;
Preconditions.checkArgument(numRetries >= 0);
int retriesRemaining = numRetries;
if (backOffPolicy != null) {
// Reset the BackOffPolicy at the start of each execute.
backOffPolicy.reset();
}
HttpResponse response = null;
IOException executeException;
Preconditions.checkNotNull(requestMethod);
Preconditions.checkNotNull(url);
Span span =
tracer
.spanBuilder(OpenCensusUtils.SPAN_NAME_HTTP_REQUEST_EXECUTE)
.setRecordEvents(OpenCensusUtils.isRecordEvent())
.startSpan();
do {
span.addAnnotation("retry #" + (numRetries - retriesRemaining));
// Cleanup any unneeded response from a previous iteration
if (response != null) {
response.ignore();
}
response = null;
executeException = null;
// run the interceptor
if (executeInterceptor != null) {
executeInterceptor.intercept(this);
}
// build low-level HTTP request
String urlString = url.build();
addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_METHOD, requestMethod);
addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_HOST, url.getHost());
addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_PATH, url.getRawPath());
addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_URL, urlString);
LowLevelHttpRequest lowLevelHttpRequest = transport.buildRequest(requestMethod, urlString);
Logger logger = HttpTransport.LOGGER;
boolean loggable = loggingEnabled && logger.isLoggable(Level.CONFIG);
StringBuilder logbuf = null;
StringBuilder curlbuf = null;
// log method and URL
if (loggable) {
logbuf = new StringBuilder();
logbuf.append("-------------- REQUEST --------------").append(StringUtils.LINE_SEPARATOR);
logbuf
.append(requestMethod)
.append(' ')
.append(urlString)
.append(StringUtils.LINE_SEPARATOR);
// setup curl logging
if (curlLoggingEnabled) {
curlbuf = new StringBuilder("curl -v --compressed");
if (!requestMethod.equals(HttpMethods.GET)) {
curlbuf.append(" -X ").append(requestMethod);
}
}
}
// add to user agent
String originalUserAgent = headers.getUserAgent();
if (!suppressUserAgentSuffix) {
if (originalUserAgent == null) {
headers.setUserAgent(USER_AGENT_SUFFIX);
addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_USER_AGENT, USER_AGENT_SUFFIX);
} else {
String newUserAgent = originalUserAgent + " " + USER_AGENT_SUFFIX;
headers.setUserAgent(newUserAgent);
addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_USER_AGENT, newUserAgent);
}
}
OpenCensusUtils.propagateTracingContext(span, headers);
// headers
HttpHeaders.serializeHeaders(headers, logbuf, curlbuf, logger, lowLevelHttpRequest);
if (!suppressUserAgentSuffix) {
// set the original user agent back so that retries do not keep appending to it
headers.setUserAgent(originalUserAgent);
}
// content
StreamingContent streamingContent = content;
final boolean contentRetrySupported = streamingContent == null || content.retrySupported();
if (streamingContent != null) {
final String contentEncoding;
long contentLength = -1;
final String contentType = content.getType();
// log content
if (loggable) {
streamingContent =
new LoggingStreamingContent(
streamingContent, HttpTransport.LOGGER, Level.CONFIG, contentLoggingLimit);
}
// encoding
if (encoding == null) {
contentEncoding = null;
contentLength = content.getLength();
} else {
contentEncoding = encoding.getName();
streamingContent = new HttpEncodingStreamingContent(streamingContent, encoding);
}
// append content headers to log buffer
if (loggable) {
if (contentType != null) {
String header = "Content-Type: " + contentType;
logbuf.append(header).append(StringUtils.LINE_SEPARATOR);
if (curlbuf != null) {
curlbuf.append(" -H '" + header + "'");
}
}
if (contentEncoding != null) {
String header = "Content-Encoding: " + contentEncoding;
logbuf.append(header).append(StringUtils.LINE_SEPARATOR);
if (curlbuf != null) {
curlbuf.append(" -H '" + header + "'");
}
}
if (contentLength >= 0) {
String header = "Content-Length: " + contentLength;
logbuf.append(header).append(StringUtils.LINE_SEPARATOR);
// do not log @ curl as the user will most likely manipulate the content
}
}
if (curlbuf != null) {
curlbuf.append(" -d '@-'");
}
// send content information to low-level HTTP request
lowLevelHttpRequest.setContentType(contentType);
lowLevelHttpRequest.setContentEncoding(contentEncoding);
lowLevelHttpRequest.setContentLength(contentLength);
lowLevelHttpRequest.setStreamingContent(streamingContent);
}
// log from buffer
if (loggable) {
logger.config(logbuf.toString());
if (curlbuf != null) {
curlbuf.append(" -- '");
curlbuf.append(urlString.replaceAll("\'", "'\"'\"'"));
curlbuf.append("'");
if (streamingContent != null) {
curlbuf.append(" << $$$");
}
logger.config(curlbuf.toString());
}
}
// We need to make sure our content type can support retry
// null content is inherently able to be retried
retryRequest = contentRetrySupported && retriesRemaining > 0;
// execute
lowLevelHttpRequest.setTimeout(connectTimeout, readTimeout);
lowLevelHttpRequest.setWriteTimeout(writeTimeout);
// switch tracing scope to current span
@SuppressWarnings("MustBeClosedChecker")
Scope ws = tracer.withSpan(span);
OpenCensusUtils.recordSentMessageEvent(span, lowLevelHttpRequest.getContentLength());
try {
LowLevelHttpResponse lowLevelHttpResponse = lowLevelHttpRequest.execute();
if (lowLevelHttpResponse != null) {
OpenCensusUtils.recordReceivedMessageEvent(span, lowLevelHttpResponse.getContentLength());
}
// Flag used to indicate if an exception is thrown before the response is constructed.
boolean responseConstructed = false;
try {
response = new HttpResponse(this, lowLevelHttpResponse);
responseConstructed = true;
} finally {
if (!responseConstructed) {
InputStream lowLevelContent = lowLevelHttpResponse.getContent();
if (lowLevelContent != null) {
lowLevelContent.close();
}
}
}
} catch (IOException e) {
if (!retryOnExecuteIOException
&& (ioExceptionHandler == null
|| !ioExceptionHandler.handleIOException(this, retryRequest))) {
// static analysis shows response is always null here
span.end(OpenCensusUtils.getEndSpanOptions(null));
throw e;
}
// Save the exception in case the retries do not work and we need to re-throw it later.
executeException = e;
if (loggable) {
logger.log(Level.WARNING, "exception thrown while executing request", e);
}
} finally {
ws.close();
}
// Flag used to indicate if an exception is thrown before the response has completed
// processing.
boolean responseProcessed = false;
try {
if (response != null && !response.isSuccessStatusCode()) {
boolean errorHandled = false;
if (unsuccessfulResponseHandler != null) {
// Even if we don't have the potential to retry, we might want to run the
// handler to fix conditions (like expired tokens) that might cause us
// trouble on our next request
errorHandled = unsuccessfulResponseHandler.handleResponse(this, response, retryRequest);
}
if (!errorHandled) {
if (handleRedirect(response.getStatusCode(), response.getHeaders())) {
// The unsuccessful request's error could not be handled and it is a redirect request.
errorHandled = true;
} else if (retryRequest
&& backOffPolicy != null
&& backOffPolicy.isBackOffRequired(response.getStatusCode())) {
// The unsuccessful request's error could not be handled and should be backed off
// before retrying
long backOffTime = backOffPolicy.getNextBackOffMillis();
if (backOffTime != BackOffPolicy.STOP) {
try {
sleeper.sleep(backOffTime);
} catch (InterruptedException exception) {
// ignore
}
errorHandled = true;
}
}
}
// A retry is required if the error was successfully handled or if it is a redirect
// request or if the back off policy determined a retry is necessary.
retryRequest &= errorHandled;
// need to close the response stream before retrying a request
if (retryRequest) {
response.ignore();
}
} else {
// Retry is not required for a successful status code unless the response is null.
retryRequest &= (response == null);
}
// Once there are no more retries remaining, this will be -1
// Count redirects as retries, we want a finite limit of redirects.
retriesRemaining--;
responseProcessed = true;
} finally {
if (response != null && !responseProcessed) {
response.disconnect();
}
}
} while (retryRequest);
span.end(OpenCensusUtils.getEndSpanOptions(response == null ? null : response.getStatusCode()));
if (response == null) {
// Retries did not help resolve the execute exception, re-throw it.
throw executeException;
}
// response interceptor
if (responseInterceptor != null) {
responseInterceptor.interceptResponse(response);
}
// throw an exception if unsuccessful response
if (throwExceptionOnExecuteError && !response.isSuccessStatusCode()) {
try {
throw new HttpResponseException(response);
} finally {
response.disconnect();
}
}
return response;
}
/**
* {@link Beta}
* Executes this request asynchronously in a single separate thread using the supplied executor.
*
* @param executor executor to run the asynchronous request
* @return future for accessing the HTTP response
* @since 1.13
*/
@Beta
public Future executeAsync(Executor executor) {
FutureTask future =
new FutureTask(
new Callable() {
public HttpResponse call() throws Exception {
return execute();
}
});
executor.execute(future);
return future;
}
/**
* {@link Beta}
* Executes this request asynchronously using {@link #executeAsync(Executor)} in a single separate
* thread using {@link Executors#newFixedThreadPool(int)}.
*
* @return A future for accessing the results of the asynchronous request.
* @since 1.13
*/
@Beta
public Future executeAsync() {
return executeAsync(
Executors.newFixedThreadPool(1, new ThreadFactoryBuilder().setDaemon(true).build()));
}
/**
* Sets up this request object to handle the necessary redirect if redirects are turned on, it is
* a redirect status code and the header has a location.
*
* When the status code is {@code 303} the method on the request is changed to a GET as per the
* RFC2616 specification. On a redirect, it also removes the {@code "Authorization"} and all
* {@code "If-*"} request headers.
*
*
Upgrade warning: When handling a status code of 303, {@link #handleRedirect(int,
* HttpHeaders)} now correctly removes any content from the body of the new request, as GET
* requests should not have content. It did not do this in prior version 1.16.
*
* @return whether the redirect was successful
* @since 1.11
*/
public boolean handleRedirect(int statusCode, HttpHeaders responseHeaders) {
String redirectLocation = responseHeaders.getLocation();
if (getFollowRedirects()
&& HttpStatusCodes.isRedirect(statusCode)
&& redirectLocation != null) {
// resolve the redirect location relative to the current location
setUrl(new GenericUrl(url.toURL(redirectLocation), useRawRedirectUrls));
// on 303 change method to GET
if (statusCode == HttpStatusCodes.STATUS_CODE_SEE_OTHER) {
setRequestMethod(HttpMethods.GET);
// GET requests do not support non-zero content length
setContent(null);
}
// remove Authorization and If-* headers
headers.setAuthorization((String) null);
headers.setIfMatch((String) null);
headers.setIfNoneMatch((String) null);
headers.setIfModifiedSince((String) null);
headers.setIfUnmodifiedSince((String) null);
headers.setIfRange((String) null);
return true;
}
return false;
}
/**
* Returns the sleeper.
*
* @since 1.15
*/
public Sleeper getSleeper() {
return sleeper;
}
/**
* Sets the sleeper. The default value is {@link Sleeper#DEFAULT}.
*
* @since 1.15
*/
public HttpRequest setSleeper(Sleeper sleeper) {
this.sleeper = Preconditions.checkNotNull(sleeper);
return this;
}
private static void addSpanAttribute(Span span, String key, String value) {
if (value != null) {
span.putAttribute(key, AttributeValue.stringAttributeValue(value));
}
}
private static String getVersion() {
// attempt to read the library's version from a properties file generated during the build
// this value should be read and cached for later use
String version = "unknown-version";
try (InputStream inputStream =
HttpRequest.class.getResourceAsStream("/google-http-client.properties")) {
if (inputStream != null) {
final Properties properties = new Properties();
properties.load(inputStream);
version = properties.getProperty("google-http-client.version");
}
} catch (IOException e) {
// ignore
}
return version;
}
}