1 // Copyright 2021 The Android Open Source Project 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.google.android.downloader; 16 17 import com.google.auto.value.AutoValue; 18 import com.google.common.base.Strings; 19 import com.google.common.net.HttpHeaders; 20 import java.net.HttpURLConnection; 21 import java.util.List; 22 import java.util.Map; 23 import javax.annotation.Nullable; 24 25 /** Simple container object that contains information about a request error. */ 26 @AutoValue 27 public abstract class ErrorDetails { 28 // Additional HTTP status codes not listed in HttpURLConnection. 29 static final int HTTP_TOO_MANY_REQUESTS = 429; 30 31 /** 32 * Returns the underlying numerical error value associated with this error. The meaning of this 33 * value depends on the network stack in use. Consult the network stack documentation to determine 34 * the meaning of this value. By default this returns the value 0. 35 */ getInternalErrorCode()36 public abstract int getInternalErrorCode(); 37 38 /** 39 * Returns the human-readable error message associated with this error. This message is for 40 * debugging purposes only and should not be parsed programmatically. By default this returns the 41 * empty string. 42 */ getErrorMessage()43 public abstract String getErrorMessage(); 44 45 /** 46 * Returns the HTTP status value associated with this error, if any. If the request succeeded but 47 * the server returned an error (e.g. server error 500), then this field can help classify the 48 * problem. If no HTTP status value is associated with this error, then the value -1 will be 49 * returned instead. 50 */ getHttpStatusCode()51 public abstract int getHttpStatusCode(); 52 53 /** 54 * Returns whether the request that triggered the error is retryable as-is without further changes 55 * to the request or state of the client. 56 * 57 * <p>Whether or not a request can be retried can depend on nuanced details of how an error 58 * occurred, so individual URL engines may make local decisions on whether an error should be 59 * retried. For example, TCP's connection reset error is often caused by a crash of the server 60 * during processing. Resending the exact same request, perhaps after some delay, has a good 61 * chance of hitting a different, healthy server and so in general this error is retryable. On the 62 * other hand, it doesn't make sense to spend resources retrying a HTTP_NOT_FOUND/404 since it 63 * isn't likely that the requested URL will spontaneously appear. A request that fails with 64 * HTTP_UNAUTHORIZED/401 is also not retryable under this definition. Although it makes sense to 65 * send an authenticated version of the failed request, this modification must happen at a higher 66 * layer. 67 */ isRetryableAsIs()68 public abstract boolean isRetryableAsIs(); 69 70 /** Creates a new builder instance for constructing request errors. */ builder()71 public static Builder builder() { 72 return new AutoValue_ErrorDetails.Builder() 73 .setInternalErrorCode(0) 74 .setErrorMessage("") 75 .setHttpStatusCode(-1) 76 .setRetryableAsIs(false); 77 } 78 79 /** 80 * Create a new error instance for the given value and message. A convenience factory function for 81 * the most common use case. 82 */ create(@ullable String message)83 public static ErrorDetails create(@Nullable String message) { 84 return builder().setErrorMessage(Strings.nullToEmpty(message)).build(); 85 } 86 87 /** 88 * Create a new error instance for an HTTP error response, as represented by the provided {@code 89 * httpResponseCode} and {@code httpResponseHeaders}. The canonical error code and retryability 90 * bit is computed based on the values of the response. 91 */ createFromHttpErrorResponse( int httpResponseCode, Map<String, List<String>> httpResponseHeaders, @Nullable String message)92 public static ErrorDetails createFromHttpErrorResponse( 93 int httpResponseCode, 94 Map<String, List<String>> httpResponseHeaders, 95 @Nullable String message) { 96 return builder() 97 .setErrorMessage(Strings.nullToEmpty(message)) 98 .setRetryableAsIs(isRetryableHttpError(httpResponseCode, httpResponseHeaders)) 99 .setHttpStatusCode(httpResponseCode) 100 .setInternalErrorCode(httpResponseCode) 101 .build(); 102 } 103 104 /** 105 * Obtains a connection error from a {@link Throwable}. If the throwable is an instance of {@link 106 * RequestException}, then it returns the error instance associated with that exception. 107 * Otherwise, a new connection error is constructed with default values and an error message set 108 * to the value returned by {@link Throwable#getMessage}. 109 */ fromThrowable(Throwable throwable)110 public static ErrorDetails fromThrowable(Throwable throwable) { 111 if (throwable instanceof RequestException) { 112 RequestException requestException = (RequestException) throwable; 113 return requestException.getErrorDetails(); 114 } else { 115 return builder().setErrorMessage(Strings.nullToEmpty(throwable.getMessage())).build(); 116 } 117 } 118 119 /** 120 * Determine if a given HTTP error, as represented by an HTTP response code and response headers, 121 * is retryable. See the comment on {@link #isRetryableAsIs} for a longer explanation on how this 122 * related to the canonical error code. 123 */ isRetryableHttpError( int httpCode, Map<String, List<String>> responseHeaders)124 private static boolean isRetryableHttpError( 125 int httpCode, Map<String, List<String>> responseHeaders) { 126 switch (httpCode) { 127 case HttpURLConnection.HTTP_CLIENT_TIMEOUT: 128 // Client timeout means some client-side timeout was encountered. Retrying is safe. 129 return true; 130 case HttpURLConnection.HTTP_ENTITY_TOO_LARGE: 131 // Entity too large means the request was too large for the server to process. Retrying is 132 // safe if the server provided the retry-after header. 133 return responseHeaders.containsKey(HttpHeaders.RETRY_AFTER); 134 case HTTP_TOO_MANY_REQUESTS: 135 // Too many requests means the server is overloaded and is rejecting requests to temporarily 136 // reduce load. See go/rfc/6585. Retrying is safe. 137 return true; 138 case HttpURLConnection.HTTP_UNAVAILABLE: 139 // Unavailable means the server is currently unable to service the request. Retrying is 140 // safe if the server provided the retry-after header. 141 return responseHeaders.containsKey(HttpHeaders.RETRY_AFTER); 142 case HttpURLConnection.HTTP_GATEWAY_TIMEOUT: 143 // Gateway timeout means there was a server timeout somewhere. Retrying is safe. 144 return true; 145 default: 146 // By default, assume any other HTTP error is not retryable. 147 return false; 148 } 149 } 150 151 /** Builder for creating instances of {@link ErrorDetails}. */ 152 @AutoValue.Builder 153 public abstract static class Builder { 154 /** Sets the error value. */ setInternalErrorCode(int internalErrorCode)155 public abstract Builder setInternalErrorCode(int internalErrorCode); 156 157 /** Sets the error message. */ setErrorMessage(String errorMessage)158 public abstract Builder setErrorMessage(String errorMessage); 159 160 /** Sets the http status value. */ setHttpStatusCode(int httpStatusCode)161 public abstract Builder setHttpStatusCode(int httpStatusCode); 162 163 /** Sets whether the error is retryable as-is. */ setRetryableAsIs(boolean retryable)164 public abstract Builder setRetryableAsIs(boolean retryable); 165 166 /** Builds the request error instance. */ build()167 public abstract ErrorDetails build(); 168 } 169 } 170