/* * Copyright 2007 Yusuke Yamamoto * * Licensed 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 twitter4j; import twitter4j.internal.http.HttpResponse; import twitter4j.internal.http.HttpResponseCode; import twitter4j.internal.json.z_T4JInternalJSONImplFactory; import twitter4j.internal.org.json.JSONException; import twitter4j.internal.org.json.JSONObject; import twitter4j.internal.json.z_T4JInternalParseUtil; import java.util.List; import static twitter4j.internal.json.z_T4JInternalParseUtil.getInt; /** * An exception class that will be thrown when TwitterAPI calls are failed.<br> * In case the Twitter server returned HTTP error code, you can get the HTTP status code using getStatusCode() method. * * @author Yusuke Yamamoto - yusuke at mac.com */ public class TwitterException extends Exception implements TwitterResponse, HttpResponseCode { private int statusCode = -1; private int errorCode = -1; private static final long serialVersionUID = -2623309261327598087L; private ExceptionDiagnosis exceptionDiagnosis = null; private HttpResponse response; private String errorMessage = null; public TwitterException(String message, Throwable cause) { super(message, cause); decode(message); } public TwitterException(String message) { this(message, (Throwable) null); } public TwitterException(Exception cause) { this(cause.getMessage(), cause); if (cause instanceof TwitterException) { ((TwitterException) cause).setNested(); } } public TwitterException(String message, HttpResponse res) { this(message); response = res; this.statusCode = res.getStatusCode(); } public TwitterException(String message, Exception cause, int statusCode) { this(message, cause); this.statusCode = statusCode; } /** * {@inheritDoc} */ @Override public String getMessage() { StringBuilder value = new StringBuilder(); if (errorMessage != null && errorCode != -1) { value.append("message - ").append(errorMessage) .append("\n"); value.append("code - ").append(errorCode) .append("\n"); } else { value.append(super.getMessage()); } if (statusCode != -1) { return getCause(statusCode) + "\n" + value.toString(); } else { return value.toString(); } } private void decode(String str) { if (str != null && str.startsWith("{")) { try { JSONObject json = new JSONObject(str); if (!json.isNull("errors")) { JSONObject error = json.getJSONArray("errors").getJSONObject(0); this.errorMessage = error.getString("message"); this.errorCode = getInt("code", error); } } catch (JSONException ignore) { } } } public int getStatusCode() { return this.statusCode; } public int getErrorCode() { return this.errorCode; } public String getResponseHeader(String name) { String value = null; if (response != null) { List<String> header = response.getResponseHeaderFields().get(name); if (header.size() > 0) { value = header.get(0); } } return value; } /** * {@inheritDoc} * * @since Twitter4J 2.1.2 */ @Override public RateLimitStatus getRateLimitStatus() { if (null == response) { return null; } return z_T4JInternalJSONImplFactory.createRateLimitStatusFromResponseHeader(response); } /** * {@inheritDoc} */ @Override public int getAccessLevel() { return z_T4JInternalParseUtil.toAccessLevel(response); } /** * Returns int value of "Retry-After" response header (Search API) or seconds_until_reset (REST API). * An application that exceeds the rate limitations of the Search API will receive HTTP 420 response codes to requests. It is a best * practice to watch for this error condition and honor the Retry-After header that instructs the application when it is safe to * continue. The Retry-After header's value is the number of seconds your application should wait before submitting another query (for * example: Retry-After: 67).<br> * Check if getStatusCode() == 503 before calling this method to ensure that you are actually exceeding rate limitation with query * apis.<br> * * @return instructs the application when it is safe to continue in seconds * @see <a href="https://dev.twitter.com/docs/rate-limiting">Rate Limiting | Twitter Developers</a> * @since Twitter4J 2.1.0 */ public int getRetryAfter() { int retryAfter = -1; if (this.statusCode == 400) { RateLimitStatus rateLimitStatus = getRateLimitStatus(); if (rateLimitStatus != null) { retryAfter = rateLimitStatus.getSecondsUntilReset(); } } else if (this.statusCode == ENHANCE_YOUR_CLAIM) { try { String retryAfterStr = response.getResponseHeader("Retry-After"); if (retryAfterStr != null) { retryAfter = Integer.valueOf(retryAfterStr); } } catch (NumberFormatException ignore) { } } return retryAfter; } /** * Tests if the exception is caused by network issue * * @return if the exception is caused by network issue * @since Twitter4J 2.1.2 */ public boolean isCausedByNetworkIssue() { return getCause() instanceof java.io.IOException; } /** * Tests if the exception is caused by rate limitation exceed * * @return if the exception is caused by rate limitation exceed * @see <a href="https://dev.twitter.com/docs/rate-limiting">Rate Limiting | Twitter Developers</a> * @since Twitter4J 2.1.2 */ public boolean exceededRateLimitation() { return (statusCode == 400 && getRateLimitStatus() != null) // REST API || (statusCode == ENHANCE_YOUR_CLAIM) // Streaming API || (statusCode == TOO_MANY_REQUESTS); // API 1.1 } /** * Tests if the exception is caused by non-existing resource * * @return if the exception is caused by non-existing resource * @since Twitter4J 2.1.2 */ public boolean resourceNotFound() { return statusCode == NOT_FOUND; } private final static String[] FILTER = new String[]{"twitter4j"}; /** * Returns a hexadecimal representation of this exception stacktrace.<br> * An exception code is a hexadecimal representation of the stacktrace which enables it easier to Google known issues.<br> * Format : XXXXXXXX:YYYYYYYY[ XX:YY]<br> * Where XX is a hash code of stacktrace without line number<br> * YY is a hash code of stacktrace excluding line number<br> * [-XX:YY] will appear when this instance a root cause * * @return a hexadecimal representation of this exception stacktrace */ public String getExceptionCode() { return getExceptionDiagnosis().asHexString(); } private ExceptionDiagnosis getExceptionDiagnosis() { if (null == exceptionDiagnosis) { exceptionDiagnosis = new ExceptionDiagnosis(this, FILTER); } return exceptionDiagnosis; } boolean nested = false; void setNested() { nested = true; } /** * Returns error message from the API if available. * * @return error message from the API * @since Twitter4J 2.2.3 */ public String getErrorMessage() { return errorMessage; } /** * Tests if error message from the API is available * * @return true if error message from the API is available * @since Twitter4J 2.2.3 */ public boolean isErrorMessageAvailable() { return errorMessage != null; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TwitterException that = (TwitterException) o; if (errorCode != that.errorCode) return false; if (nested != that.nested) return false; if (statusCode != that.statusCode) return false; if (errorMessage != null ? !errorMessage.equals(that.errorMessage) : that.errorMessage != null) return false; if (exceptionDiagnosis != null ? !exceptionDiagnosis.equals(that.exceptionDiagnosis) : that.exceptionDiagnosis != null) return false; if (response != null ? !response.equals(that.response) : that.response != null) return false; return true; } @Override public int hashCode() { int result = statusCode; result = 31 * result + errorCode; result = 31 * result + (exceptionDiagnosis != null ? exceptionDiagnosis.hashCode() : 0); result = 31 * result + (response != null ? response.hashCode() : 0); result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0); result = 31 * result + (nested ? 1 : 0); return result; } @Override public String toString() { return getMessage() + (nested ? "" : "\nRelevant discussions can be found on the Internet at:\n" + "\thttp://www.google.co.jp/search?q=" + getExceptionDiagnosis().getStackLineHashAsHex() + " or\n\thttp://www.google.co.jp/search?q=" + getExceptionDiagnosis().getLineNumberHashAsHex()) + "\nTwitterException{" + (nested ? "" : "exceptionCode=[" + getExceptionCode() + "], ") + "statusCode=" + statusCode + ", message=" + errorMessage + ", code=" + errorCode + ", retryAfter=" + getRetryAfter() + ", rateLimitStatus=" + getRateLimitStatus() + ", version=" + Version.getVersion() + '}'; } private static String getCause(int statusCode) { String cause; // https://dev.twitter.com/docs/error-codes-responses switch (statusCode) { case NOT_MODIFIED: cause = "There was no new data to return."; break; case BAD_REQUEST: cause = "The request was invalid. An accompanying error message will explain why. This is the status code will be returned during version 1.0 rate limiting(https://dev.twitter.com/pages/rate-limiting). In API v1.1, a request without authentication is considered invalid and you will get this response."; break; case UNAUTHORIZED: cause = "Authentication credentials (https://dev.twitter.com/pages/auth) were missing or incorrect. Ensure that you have set valid consumer key/secret, access token/secret, and the system clock is in sync."; break; case FORBIDDEN: cause = "The request is understood, but it has been refused. An accompanying error message will explain why. This code is used when requests are being denied due to update limits (https://support.twitter.com/articles/15364-about-twitter-limits-update-api-dm-and-following)."; break; case NOT_FOUND: cause = "The URI requested is invalid or the resource requested, such as a user, does not exists. Also returned when the requested format is not supported by the requested method."; break; case NOT_ACCEPTABLE: cause = "Returned by the Search API when an invalid format is specified in the request.\n" + "Returned by the Streaming API when one or more of the parameters are not suitable for the resource. The track parameter, for example, would throw this error if:\n" + " The track keyword is too long or too short.\n" + " The bounding box specified is invalid.\n" + " No predicates defined for filtered resource, for example, neither track nor follow parameter defined.\n" + " Follow userid cannot be read."; break; case ENHANCE_YOUR_CLAIM: cause = "Returned by the Search and Trends API when you are being rate limited (https://dev.twitter.com/docs/rate-limiting).\n" + "Returned by the Streaming API:\n Too many login attempts in a short period of time.\n" + " Running too many copies of the same application authenticating with the same account name."; break; case UNPROCESSABLE_ENTITY: cause = "Returned when an image uploaded to POST account/update_profile_banner(https://dev.twitter.com/docs/api/1/post/account/update_profile_banner) is unable to be processed."; break; case TOO_MANY_REQUESTS: cause = "Returned in API v1.1 when a request cannot be served due to the application's rate limit having been exhausted for the resource. See Rate Limiting in API v1.1.(https://dev.twitter.com/docs/rate-limiting/1.1)"; break; case INTERNAL_SERVER_ERROR: cause = "Something is broken. Please post to the group (https://dev.twitter.com/docs/support) so the Twitter team can investigate."; break; case BAD_GATEWAY: cause = "Twitter is down or being upgraded."; break; case SERVICE_UNAVAILABLE: cause = "The Twitter servers are up, but overloaded with requests. Try again later."; break; case GATEWAY_TIMEOUT: cause = "The Twitter servers are up, but the request couldn't be serviced due to some failure within our stack. Try again later."; break; default: cause = ""; } return statusCode + ":" + cause; } }