/** * Copyright 2010-present Facebook. * * 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 com.facebook; import com.facebook.android.R; import com.facebook.internal.Utility; import org.json.JSONException; import org.json.JSONObject; import java.net.HttpURLConnection; /** * This class represents an error that occurred during a Facebook request. * <p/> * In general, one would call {@link #getCategory()} to determine the type * of error that occurred, and act accordingly. The app can also call * {@link #getUserActionMessageId()} in order to get the resource id for a * string that can be displayed to the user. For more information on error * handling, see <a href="https://developers.facebook.com/docs/reference/api/errors/"> * https://developers.facebook.com/docs/reference/api/errors/</a> */ public final class FacebookRequestError { /** Represents an invalid or unknown error code from the server. */ public static final int INVALID_ERROR_CODE = -1; /** * Indicates that there was no valid HTTP status code returned, indicating * that either the error occurred locally, before the request was sent, or * that something went wrong with the HTTP connection. Check the exception * from {@link #getException()}; */ public static final int INVALID_HTTP_STATUS_CODE = -1; private static final int INVALID_MESSAGE_ID = 0; private static final String CODE_KEY = "code"; private static final String BODY_KEY = "body"; private static final String ERROR_KEY = "error"; private static final String ERROR_TYPE_FIELD_KEY = "type"; private static final String ERROR_CODE_FIELD_KEY = "code"; private static final String ERROR_MESSAGE_FIELD_KEY = "message"; private static final String ERROR_CODE_KEY = "error_code"; private static final String ERROR_SUB_CODE_KEY = "error_subcode"; private static final String ERROR_MSG_KEY = "error_msg"; private static final String ERROR_REASON_KEY = "error_reason"; private static class Range { private final int start, end; private Range(int start, int end) { this.start = start; this.end = end; } boolean contains(int value) { return start <= value && value <= end; } } private static final int EC_UNKNOWN_ERROR = 1; private static final int EC_SERVICE_UNAVAILABLE = 2; private static final int EC_APP_TOO_MANY_CALLS = 4; private static final int EC_USER_TOO_MANY_CALLS = 17; private static final int EC_PERMISSION_DENIED = 10; private static final int EC_INVALID_SESSION = 102; private static final int EC_INVALID_TOKEN = 190; private static final Range EC_RANGE_PERMISSION = new Range(200, 299); private static final int EC_APP_NOT_INSTALLED = 458; private static final int EC_USER_CHECKPOINTED = 459; private static final int EC_PASSWORD_CHANGED = 460; private static final int EC_EXPIRED = 463; private static final int EC_UNCONFIRMED_USER = 464; private static final Range HTTP_RANGE_SUCCESS = new Range(200, 299); private static final Range HTTP_RANGE_CLIENT_ERROR = new Range(400, 499); private static final Range HTTP_RANGE_SERVER_ERROR = new Range(500, 599); private final int userActionMessageId; private final boolean shouldNotifyUser; private final Category category; private final int requestStatusCode; private final int errorCode; private final int subErrorCode; private final String errorType; private final String errorMessage; private final JSONObject requestResult; private final JSONObject requestResultBody; private final Object batchRequestResult; private final HttpURLConnection connection; private final FacebookException exception; private FacebookRequestError(int requestStatusCode, int errorCode, int subErrorCode, String errorType, String errorMessage, JSONObject requestResultBody, JSONObject requestResult, Object batchRequestResult, HttpURLConnection connection, FacebookException exception) { this.requestStatusCode = requestStatusCode; this.errorCode = errorCode; this.subErrorCode = subErrorCode; this.errorType = errorType; this.errorMessage = errorMessage; this.requestResultBody = requestResultBody; this.requestResult = requestResult; this.batchRequestResult = batchRequestResult; this.connection = connection; boolean isLocalException = false; if (exception != null) { this.exception = exception; isLocalException = true; } else { this.exception = new FacebookServiceException(this, errorMessage); } // Initializes the error categories based on the documented error codes as outlined here // https://developers.facebook.com/docs/reference/api/errors/ Category errorCategory = null; int messageId = INVALID_MESSAGE_ID; boolean shouldNotify = false; if (isLocalException) { errorCategory = Category.CLIENT; messageId = INVALID_MESSAGE_ID; } else { if (errorCode == EC_UNKNOWN_ERROR || errorCode == EC_SERVICE_UNAVAILABLE) { errorCategory = Category.SERVER; } else if (errorCode == EC_APP_TOO_MANY_CALLS || errorCode == EC_USER_TOO_MANY_CALLS) { errorCategory = Category.THROTTLING; } else if (errorCode == EC_PERMISSION_DENIED || EC_RANGE_PERMISSION.contains(errorCode)) { errorCategory = Category.PERMISSION; messageId = R.string.com_facebook_requesterror_permissions; } else if (errorCode == EC_INVALID_SESSION || errorCode == EC_INVALID_TOKEN) { if (subErrorCode == EC_USER_CHECKPOINTED || subErrorCode == EC_UNCONFIRMED_USER) { errorCategory = Category.AUTHENTICATION_RETRY; messageId = R.string.com_facebook_requesterror_web_login; shouldNotify = true; } else { errorCategory = Category.AUTHENTICATION_REOPEN_SESSION; if ((subErrorCode == EC_APP_NOT_INSTALLED) || (subErrorCode == EC_EXPIRED)) { messageId = R.string.com_facebook_requesterror_relogin; } else if (subErrorCode == EC_PASSWORD_CHANGED) { messageId = R.string.com_facebook_requesterror_password_changed; } else { messageId = R.string.com_facebook_requesterror_reconnect; shouldNotify = true; } } } if (errorCategory == null) { if (HTTP_RANGE_CLIENT_ERROR.contains(requestStatusCode)) { errorCategory = Category.BAD_REQUEST; } else if (HTTP_RANGE_SERVER_ERROR.contains(requestStatusCode)) { errorCategory = Category.SERVER; } else { errorCategory = Category.OTHER; } } } this.category = errorCategory; this.userActionMessageId = messageId; this.shouldNotifyUser = shouldNotify; } private FacebookRequestError(int requestStatusCode, int errorCode, int subErrorCode, String errorType, String errorMessage, JSONObject requestResultBody, JSONObject requestResult, Object batchRequestResult, HttpURLConnection connection) { this(requestStatusCode, errorCode, subErrorCode, errorType, errorMessage, requestResultBody, requestResult, batchRequestResult, connection, null); } FacebookRequestError(HttpURLConnection connection, Exception exception) { this(INVALID_HTTP_STATUS_CODE, INVALID_ERROR_CODE, INVALID_ERROR_CODE, null, null, null, null, null, connection, (exception instanceof FacebookException) ? (FacebookException) exception : new FacebookException(exception)); } public FacebookRequestError(int errorCode, String errorType, String errorMessage) { this(INVALID_HTTP_STATUS_CODE, errorCode, INVALID_ERROR_CODE, errorType, errorMessage, null, null, null, null, null); } /** * Returns the resource id for a user-friendly message for the application to * present to the user. * * @return a user-friendly message to present to the user */ public int getUserActionMessageId() { return userActionMessageId; } /** * Returns whether direct user action is required to successfully continue with the Facebook * operation. If user action is required, apps can also call {@link #getUserActionMessageId()} * in order to get a resource id for a message to show the user. * * @return whether direct user action is required */ public boolean shouldNotifyUser() { return shouldNotifyUser; } /** * Returns the category in which the error belongs. Applications can use the category * to determine how best to handle the errors (e.g. exponential backoff for retries if * being throttled). * * @return the category in which the error belong */ public Category getCategory() { return category; } /** * Returns the HTTP status code for this particular request. * * @return the HTTP status code for the request */ public int getRequestStatusCode() { return requestStatusCode; } /** * Returns the error code returned from Facebook. * * @return the error code returned from Facebook */ public int getErrorCode() { return errorCode; } /** * Returns the sub-error code returned from Facebook. * * @return the sub-error code returned from Facebook */ public int getSubErrorCode() { return subErrorCode; } /** * Returns the type of error as a raw string. This is generally less useful * than using the {@link #getCategory()} method, but can provide further details * on the error. * * @return the type of error as a raw string */ public String getErrorType() { return errorType; } /** * Returns the error message returned from Facebook. * * @return the error message returned from Facebook */ public String getErrorMessage() { if (errorMessage != null) { return errorMessage; } else { return exception.getLocalizedMessage(); } } /** * Returns the body portion of the response corresponding to the request from Facebook. * * @return the body of the response for the request */ public JSONObject getRequestResultBody() { return requestResultBody; } /** * Returns the full JSON response for the corresponding request. In a non-batch request, * this would be the raw response in the form of a JSON object. In a batch request, this * result will contain the body of the response as well as the HTTP headers that pertain * to the specific request (in the form of a "headers" JSONArray). * * @return the full JSON response for the request */ public JSONObject getRequestResult() { return requestResult; } /** * Returns the full JSON response for the batch request. If the request was not a batch * request, then the result from this method is the same as {@link #getRequestResult()}. * In case of a batch request, the result will be a JSONArray where the elements * correspond to the requests in the batch. Callers should check the return type against * either JSONObject or JSONArray and cast accordingly. * * @return the full JSON response for the batch */ public Object getBatchRequestResult() { return batchRequestResult; } /** * Returns the HTTP connection that was used to make the request. * * @return the HTTP connection used to make the request */ public HttpURLConnection getConnection() { return connection; } /** * Returns the exception associated with this request, if any. * * @return the exception associated with this request */ public FacebookException getException() { return exception; } @Override public String toString() { return new StringBuilder("{HttpStatus: ") .append(requestStatusCode) .append(", errorCode: ") .append(errorCode) .append(", errorType: ") .append(errorType) .append(", errorMessage: ") .append(getErrorMessage()) .append("}") .toString(); } static FacebookRequestError checkResponseAndCreateError(JSONObject singleResult, Object batchResult, HttpURLConnection connection) { try { if (singleResult.has(CODE_KEY)) { int responseCode = singleResult.getInt(CODE_KEY); Object body = Utility.getStringPropertyAsJSON(singleResult, BODY_KEY, Response.NON_JSON_RESPONSE_PROPERTY); if (body != null && body instanceof JSONObject) { JSONObject jsonBody = (JSONObject) body; // Does this response represent an error from the service? We might get either an "error" // with several sub-properties, or else one or more top-level fields containing error info. String errorType = null; String errorMessage = null; int errorCode = INVALID_ERROR_CODE; int errorSubCode = INVALID_ERROR_CODE; boolean hasError = false; if (jsonBody.has(ERROR_KEY)) { // We assume the error object is correctly formatted. JSONObject error = (JSONObject) Utility.getStringPropertyAsJSON(jsonBody, ERROR_KEY, null); errorType = error.optString(ERROR_TYPE_FIELD_KEY, null); errorMessage = error.optString(ERROR_MESSAGE_FIELD_KEY, null); errorCode = error.optInt(ERROR_CODE_FIELD_KEY, INVALID_ERROR_CODE); errorSubCode = error.optInt(ERROR_SUB_CODE_KEY, INVALID_ERROR_CODE); hasError = true; } else if (jsonBody.has(ERROR_CODE_KEY) || jsonBody.has(ERROR_MSG_KEY) || jsonBody.has(ERROR_REASON_KEY)) { errorType = jsonBody.optString(ERROR_REASON_KEY, null); errorMessage = jsonBody.optString(ERROR_MSG_KEY, null); errorCode = jsonBody.optInt(ERROR_CODE_KEY, INVALID_ERROR_CODE); errorSubCode = jsonBody.optInt(ERROR_SUB_CODE_KEY, INVALID_ERROR_CODE); hasError = true; } if (hasError) { return new FacebookRequestError(responseCode, errorCode, errorSubCode, errorType, errorMessage, jsonBody, singleResult, batchResult, connection); } } // If we didn't get error details, but we did get a failure response code, report it. if (!HTTP_RANGE_SUCCESS.contains(responseCode)) { return new FacebookRequestError(responseCode, INVALID_ERROR_CODE, INVALID_ERROR_CODE, null, null, singleResult.has(BODY_KEY) ? (JSONObject) Utility.getStringPropertyAsJSON( singleResult, BODY_KEY, Response.NON_JSON_RESPONSE_PROPERTY) : null, singleResult, batchResult, connection); } } } catch (JSONException e) { // defer the throwing of a JSONException to the graph object proxy } return null; } /** * An enum that represents the Facebook SDK classification for the error that occurred. */ public enum Category { /** * Indicates that the error is authentication related, and that the app should retry * the request after some user action. */ AUTHENTICATION_RETRY, /** * Indicates that the error is authentication related, and that the app should close * the session and reopen it. */ AUTHENTICATION_REOPEN_SESSION, /** Indicates that the error is permission related. */ PERMISSION, /** * Indicates that the error implies the server had an unexpected failure or may be * temporarily unavailable. */ SERVER, /** Indicates that the error results from the server throttling the client. */ THROTTLING, /** * Indicates that the error is Facebook-related but cannot be categorized at this time, * and is likely newer than the current version of the SDK. */ OTHER, /** * Indicates that the error is an application error resulting in a bad or malformed * request to the server. */ BAD_REQUEST, /** * Indicates that this is a client-side error. Examples of this can include, but are * not limited to, JSON parsing errors or {@link java.io.IOException}s. */ CLIENT }; }