All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.treasuredata.client.TDRequestErrorHandler Maven / Gradle / Ivy

There is a newer version: 1.1.1
Show newest version
package com.treasuredata.client;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.treasuredata.client.model.TDApiErrorMessage;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLKeyException;
import javax.net.ssl.SSLPeerUnverifiedException;

import java.io.EOFException;
import java.io.IOException;
import java.net.BindException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.PortUnreachableException;
import java.net.ProtocolException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static com.google.common.net.HttpHeaders.RETRY_AFTER;
import static com.treasuredata.client.TDClientException.ErrorType.CLIENT_ERROR;
import static com.treasuredata.client.TDClientException.ErrorType.INVALID_INPUT;
import static com.treasuredata.client.TDClientException.ErrorType.INVALID_JSON_RESPONSE;
import static com.treasuredata.client.TDClientException.ErrorType.PROXY_AUTHENTICATION_FAILURE;
import static com.treasuredata.client.TDClientException.ErrorType.SERVER_ERROR;
import static com.treasuredata.client.TDClientException.ErrorType.UNEXPECTED_RESPONSE_CODE;
import static com.treasuredata.client.TDClientHttpTooManyRequestsException.TOO_MANY_REQUESTS_429;
import static com.treasuredata.client.TDHttpRequestHandler.ResponseContext;

/**
 * TDRequestErrorHandler has a logic to handle http request retris,
 */
public class TDRequestErrorHandler
{
    private TDRequestErrorHandler()
    {
    }

    // Use TDHttpClient's logger for better log messages
    private static Logger logger = LoggerFactory.getLogger(TDHttpClient.class);

    @VisibleForTesting
    static final ThreadLocal HTTP_DATE_FORMAT = new ThreadLocal()
    {
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
        }
    };

    /**
     * Show or suppress warning messages for TDClientHttpException
     */
    private static TDClientHttpException clientError(TDClientHttpException e, ResponseContext responseContext)
    {
        boolean showWarning = true;
        boolean showStackTrace = false;
        int code = e.getStatusCode();
        switch (code) {
            case HttpStatus.NOT_FOUND_404:
                // Not found (e.g., database, table, table distribution data might be missing)
                showWarning = false;
                break;
            case HttpStatus.CONFLICT_409:
                // Suppress stack trace because 409 frequently happens when checking the presence of databases and tables.
                showStackTrace = false;
                break;
            default:
                if (!(HttpStatus.isClientError(code) || HttpStatus.isServerError(code))) {
                    // Show stack trace for non 4xx, 5xx errors
                    showStackTrace = true;
                }
                break;
        }
        if (showWarning) {
            if (showStackTrace) {
                logger.warn(e.getCause() == null ? e.getMessage() : e.getCause().getClass().toString(), e);
            }
            else {
                logger.warn(e.getCause() == null ? e.getMessage() : e.getCause().getClass().toString());
            }
        }
        return e;
    }

    public static TDClientException defaultHttpResponseErrorResolver(ResponseContext responseContext)
            throws TDClientException
    {
        Response response = responseContext.response;
        int code = response.code();
        long now = System.currentTimeMillis();

        Date retryAfter = parseRetryAfter(now, response);
        Optional errorResponse = extractErrorResponse(responseContext.response);
        String responseErrorText = errorResponse.isPresent() ? ": " + errorResponse.get().getText() : "";
        String errorMessage = String.format("[%d:%s] API request to %s has failed%s", code, HttpStatus.getMessage(code), responseContext.apiRequest.getPath(), responseErrorText);
        if (HttpStatus.isClientError(code)) {
            logger.debug(errorMessage);
            switch (code) {
                // Soft 4xx errors. These we retry.
                case TOO_MANY_REQUESTS_429:
                    return clientError(new TDClientHttpTooManyRequestsException(errorMessage, retryAfter), responseContext);
                // Hard 4xx error. We do not retry the execution on this type of error
                case HttpStatus.UNAUTHORIZED_401:
                    throw clientError(new TDClientHttpUnauthorizedException(errorMessage), responseContext);
                case HttpStatus.NOT_FOUND_404:
                    throw clientError(new TDClientHttpNotFoundException(errorMessage), responseContext);
                case HttpStatus.CONFLICT_409:
                    String conflictsWith = errorResponse.isPresent() ? parseConflictsWith(errorResponse.get()) : null;
                    throw clientError(new TDClientHttpConflictException(errorMessage, conflictsWith), responseContext);
                case HttpStatus.PROXY_AUTHENTICATION_REQUIRED_407:
                    throw clientError(new TDClientHttpException(PROXY_AUTHENTICATION_FAILURE, errorMessage, code, retryAfter), responseContext);
                case HttpStatus.UNPROCESSABLE_ENTITY_422:
                    throw clientError(new TDClientHttpException(INVALID_INPUT, errorMessage, code, retryAfter), responseContext);
                default:
                    throw clientError(new TDClientHttpException(CLIENT_ERROR, errorMessage, code, retryAfter), responseContext);
            }
        }
        else if (HttpStatus.isServerError(code)) {
            // Just returns exception info for 5xx errors
            return clientError(new TDClientHttpException(SERVER_ERROR, errorMessage, code, retryAfter), responseContext);
        }
        else {
            throw clientError(new TDClientHttpException(UNEXPECTED_RESPONSE_CODE, errorMessage, code, retryAfter), responseContext);
        }
    }

    public static TDClientException defaultErrorResolver(Throwable e)
            throws TDClientException
    {
        if (e instanceof Exception) {
            return defaultExceptionResolver((Exception) e);
        }
        else {
            throw new TDClientProcessingException(new RuntimeException(e));
        }
    }

    /**
     * @return If the error type is retryable, return the exception. If not, throw it as TDClientException
     * @throws TDClientException
     */
    public static TDClientException defaultExceptionResolver(Exception e)
            throws TDClientException
    {
        if (TDClientException.class.isAssignableFrom(e.getClass())) {
            // If the error is known error, we should throw it as is
            throw (TDClientException) e;
        }
        else if (e instanceof ProtocolException || e instanceof ConnectException || e instanceof EOFException) {
            // OkHttp throws ProtocolException the content length is insufficient
            // ConnectionException can be throw if server is shutting down
            // EOFException can be thrown when the connection was interrupted
            return new TDClientInterruptedException("connection failure", e);
        }
        else if (e instanceof TimeoutException || e instanceof SocketTimeoutException) {
            // OkHttp throws SocketTimeoutException
            return new TDClientTimeoutException(e);
        }
        else if (e instanceof SocketException) {
            final SocketException socketException = (SocketException) e;
            if (socketException instanceof BindException ||
                    socketException instanceof ConnectException ||
                    socketException instanceof NoRouteToHostException ||
                    socketException instanceof PortUnreachableException) {
                // All known SocketException are retryable.
                return new TDClientSocketException(socketException);
            }
            else {
                // Other unknown SocketException are considered non-retryable.
                throw new TDClientSocketException(socketException);
            }
        }
        else if (e instanceof SSLException) {
            SSLException sslException = (SSLException) e;
            if (sslException instanceof SSLHandshakeException || sslException instanceof SSLKeyException || sslException instanceof SSLPeerUnverifiedException) {
                // deterministic SSL exceptions
                throw new TDClientSSLException(sslException);
            }
            else {
                // SSLProtocolException and uncategorized SSL exceptions (SSLException) such as unexpected_message may be retryable
                return new TDClientSSLException(sslException);
            }
        }
        else if (e.getCause() != null && Exception.class.isAssignableFrom(e.getCause().getClass())) {
            return defaultExceptionResolver((Exception) e.getCause());
        }
        else {
            logger.warn("unknown type exception: " + e.getClass(), e);
            throw new TDClientProcessingException(e);
        }
    }

    /**
     * https://tools.ietf.org/html/rfc7231#section-7.1.3
     */
    @VisibleForTesting
    static Date parseRetryAfter(long now, Response response)
    {
        String retryAfter = response.header(RETRY_AFTER);
        if (retryAfter == null) {
            return null;
        }
        // Try parsing as a number of seconds first
        try {
            long retryAfterSeconds = Long.parseLong(retryAfter);
            return new Date(now + TimeUnit.SECONDS.toMillis(retryAfterSeconds));
        }
        catch (NumberFormatException e) {
            // Then try parsing as a HTTP-date
            try {
                return HTTP_DATE_FORMAT.get().parse(retryAfter);
            }
            catch (ParseException ignore) {
                logger.warn("Failed to parse Retry-After header: '" + retryAfter + "'");
                return null;
            }
        }
    }

    private static String parseConflictsWith(TDApiErrorMessage errorResponse)
    {
        Map details = errorResponse.getDetails();
        if (details == null) {
            return null;
        }
        Object conflictsWith = details.get("conflicts_with");
        if (conflictsWith == null) {
            return null;
        }
        return String.valueOf(conflictsWith);
    }

    @VisibleForTesting
    public static Optional extractErrorResponse(Response response)
    {
        Optional content = Optional.absent();
        try {
            try {
                content = Optional.of(response.body().string());
            }
            catch (IOException e) {
                throw new TDClientException(INVALID_JSON_RESPONSE, e);
            }

            if (content.isPresent() && content.get().length() > 0 && content.get().charAt(0) == '{') {
                // Error message from TD API
                return Optional.of(TDHttpClient.defaultObjectMapper.readValue(content.get(), TDApiErrorMessage.class));
            }
            else {
                // Error message from Proxy server etc.
                return Optional.of(new TDApiErrorMessage("error", content.or("[empty]"), "error"));
            }
        }
        catch (IOException e) {
            logger.warn("Failed to parse the error response {}: {}\n{}", response.request().url(), content.or("[empty]"), e.getMessage());
        }
        return Optional.absent();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy