I want to handle error in Retrofit 2.0
Got e.g. code=404 and body=null, but errorBody() contains data in ErrorModel (Boolean status and String info).
This is errorBody().content: [text=\n{"status":false,"info":"Provided email doesn't exist."}].
How can I get this data?
Thank for helping me!
This is my code for Retrofit request:
ResetPasswordApi.Factory.getInstance().resetPassword(loginEditText.getText().toString())
.enqueue(new Callback<StatusInfoModel>() {
#Override
public void onResponse(Call<StatusInfoModel> call, Response<StatusInfoModel> response) {
if (response.isSuccessful()) {
showToast(getApplicationContext(), getString(R.string.new_password_sent));
} else {
showToast(getApplicationContext(), getString(R.string.email_not_exist));
}
}
#Override
public void onFailure(Call<StatusInfoModel> call, Throwable t) {
showToast(getApplicationContext(), "Something went wrong...");
}
});
If you want to get data when error response comes (typically a response code except 200) you can do it like that in your onResponse() method:
if (response.code() == 404) {
Gson gson = new GsonBuilder().create();
YourErrorPojo pojo = new YourErrorPojo();
try {
pojo = gson.fromJson(response.errorBody().string(), YourErrorPojo.class);
Toast.makeText(context, pojo.getInfo(), Toast.LENGTH_LONG).show();
} catch (IOException e) {
// handle failure at error parse
}
}
When generating YourErrorPojo.class do following steps :
Go to Json Schema 2 Pojo
Paste your example Json, and select source type Json , annotation Gson
Your example Json is : {"status":false,"info":"Provided email doesn't exist."}
Click Preview and it will generate your Pojo class for you.
Add this to your build.gradle : compile 'com.google.code.gson:gson:2.7'
I used Gson in this solution but you can get your Json string using: response.errorBody().string()
Retrofit doesn't see 404 as a failure, so it will enter the onSuccess.
response.isSuccessful() is true if the response code is in the range of 200-300, so it will enter the else there.
if (response.isSuccessful()) {
showToast(getApplicationContext(), getString(R.string.new_password_sent));
} else {
// A 404 will go here
showToast(getApplicationContext(), getString(R.string.email_not_exist));
}
However since your response was not successful, you do not get the response body with .body(), but with errorBody(), errorBody will filled when the request was a success, but response.isSuccessful() returns false (so in case of a status code that is not 200-300).
I'm using this library Retrobomb, you don't have to serialize at that level.
it's easy to use and customize. It supports annotation for each error type or error code.
If you prefer you can unwrap all errors and handle by your self.
#ErrorMapping(code = 401, errorType = Unauthorized.class)
#PATCH("/v1/widgets/{id}")
Single<Widget> updateWidget(#Path("id") String id, #Body Widget widget);
If you want to get data when error response comes (typically a response code except 200) you can do it like that in your onResponse() method:
override fun onResponse(call: Call<LoginData>?, response: Response<LoginData>?) {
if (response != null) {
if (response.code() == 200 && response.body() != null) {
val loginData = response.body()
if (loginData != null) {
//Handle success case...
}
} else if (response.code() == 401) {
val converter = ApiClient.getClient()?.responseBodyConverter<ErrorResponseData>(
ErrorResponseData::class.java,
arrayOfNulls<Annotation>(0))
var errorResponse: ErrorResponseData? = null
errorResponse = converter?.convert(response.errorBody())
if (errorResponse != null) {
//Handle Error case..
}
}
}
}
For Kotlin:
Just follow this code to convert your errorBody to your response:
if(response.isSuccessful){
val data = response.body()!!
}else {
val gson = GsonBuilder().create()
try {
var pojo = gson.fromJson(
response.errorBody()!!.string(),
CommentResponse::class.java)
Log.e("ERROR_CHECK","here else is the error$pojo")
} catch (e: IOException) {
// handle failure at error parse
}
}
you can do in the following way
fun parseErrorBody(readText: String?): String {
if (!readText.isNullOrEmpty()) {
val result = Gson().fromJson(readText, AppError::class.java)//AppError can be your server error response model
return result.errors?.get(0)?.message ?: Constants.EMPTY_STR
}
return ""
}
and calling code
if(response.isSuccessful){}//handle success response
else{
parseErrorBody(response.errorBody()?.charStream()?.readText())
}
Related
Ive created an interceptor. In some cases, I want to retry the request 'n' number of time how do i do this?
class NetworkErrorHandler constructor(): Interceptor {
//Central error handling block for errors which has impact all over the app
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response = chain.proceed(request)
return
when (response.code) {
401 -> {
response
}
200 ->{
response
}
else -> {
var tryCount = 0
while (tryCount < 3) {
try {
response = chain.proceed(request)
tryCount++
}catch (e: java.lang.Exception){
tryCount++
}
}
response
}
}
}
}
It gives me this error:
Suppressed: java.lang.IllegalStateException: network interceptor must call proceed() exactly once
Do I have to do this here if yes, then how?
So i was able to make another call from the interceptor by using this line
response.close()
chain.call().clone().execute()
Full code according to the question:
//Central error handling block for errors which has impact all over the app
class NetworkErrorHandler constructor(): Interceptor {
var tryCount = 0
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response = chain.proceed(request)
return
when(response.code) {
401 - > {
response
}
200 - > {
response
}
else - > {
if (tryCount < 3) {
Thread.sleep(2000)
tryCount++
response.close()
chain.call().clone().execute()
}
response.newBuilder()
.code(401) // Whatever code
.body("".toResponseBody(null)) // Whatever body
.protocol(Protocol.HTTP_2)
.message("Network Error")
.request(chain.request())
.build()
}
}
}
Use .interceptors() to use Application Interceptor instead of .networkInterceptors() which are allowed to call .proceed() more than once.
More information: https://square.github.io/okhttp/interceptors/
I think you guided yourself from this other question How to retry HTTP requests with OkHttp/Retrofit? (If not, I suggest you do). The problem on your code is that your while is restricted only by trycount, but not by the result of the response. Try adding the validation regarding the status of the response in there too, so it doesn't execute the .proceed method two times in parallel.
// try the request
Response response = chain.proceed(request);
int tryCount = 0;
while (!response.isSuccessful() && tryCount < 3) {
This is how I approached it:
#Slf4j
public class HttpRetryInterceptor implements Interceptor {
#Override
public #NotNull Response intercept(Chain chain) throws IOException {
log.error("Making request for the first time.");
var request = chain.request();
Response response = null;
boolean responseOK = false;
byte tryCount = 0;
while (!responseOK && tryCount < 3) {
try {
Thread.sleep(5000 * tryCount);
response = chain.proceed(request);
log.info("Response is: {}",response);
log.info("Response message: {}",response.message());
responseOK = response.isSuccessful();
}catch (Exception e){
e.printStackTrace();
log.error("Request was not successful: {} . Retrying." , tryCount);
}finally{
assert response != null;
response.close();
tryCount++;
}
}
return response != null ? response : chain.proceed(request);
}
}
And then I added my Interceptor to my client with something like : new OkHttpClient.Builder().addInterceptor(new HttpRetryInterceptor()).build()
An API we use to login a user recently changed where if they need to take an action, a string is sent in the body instead of a JSON (which gets converted to an object). Therefore, I need to be able to handle this.
I changed my observable response to handle ANY like so:
#POST("api/v1/user/login")
fun postLogin(#Body body: LoginBody): Observable<Response<Any>>
but when I go to subscribe on this observable I am getting an exception that JSON has not been fully consumed.
postLogin(LoginBody(username, password))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
if (it.isSuccessful) {
val res = LoginRemote.parseResponse(it as Response<LoginRemote>)
if (res.result is Result.Ok) {
res.result.data.uid?.let { sessionData?.uid = it }
res.result.data.username?.let { sessionData?.username = it }
res.result.data.email?.let { sessionData?.email = it }
res.result.data.phoneType?.let { sessionData?.phoneType = it }
res.result.data.phone?.let { sessionData?.phone = it }
res.result.data.verified?.let { sessionData?.verified = it }
res.result.data.role?.let { sessionData?.role = it }
res.result.data.gender?.let { sessionData?.gender = it }
res.result.data.countryOfResidence?.let { sessionData?.countryOfResidence = it }
}
acceptTermsResponse.value = res
} else {
acceptTermsResponse.value = LoginResponse(listOf(LoginResponse.ErrorType.Generic()))
}
}, {
ERRORS HERE --> acceptTermsResponse.value = LoginResponse(listOf(LoginResponse.ErrorType.Generic()))
})
Is there a way to subscribe to a two types of response data?
I found this post and attempted the solution of adding the Scalar Converter Factory to my retrofit but that didn't solve the problem.
I am using Retrofit (2.6) on Android to implement a service which connects to a web server, and which requests that the server undertake some work. The relevant code can be summarized thus:
interface MyService {
#GET(START_WORK)
suspend fun startWork(#Query("uuid") uuid: String,
#Query("mode") mode: Int):
MyStartWorkResponse
}
// Do some other things, include get a reference to a properly configured
// instance of Retrofit.
// Instantiate service
var service: MyService = retrofit.create(MyService::class.java)
I can call service.startWork() with no problem and obtain valid results. However, in some conditions, the web server will return a 400 error code, with a response body which includes specific error information. The request is not malformed, however; it's just that there is another problem which should be brought to the user's attention. The trouble is, I can't tell what the problem is, because I don't get a response; instead, my call throws an exception because of the 400 error code.
I don't understand how to modify my code so that I can catch and handle 400 error responses, and get the information I need from the body of the response. Is this a job for a network interceptor on my okhttp client? Can anyone shed some light?
Use this code (KOTLIN)
class ApiClient {
companion object {
private val BASE_URL = "YOUR_URL_SERVER"
private var retrofit: Retrofit? = null
private val okHttpClientvalor = OkHttpClient.Builder()
.connectTimeout(90, TimeUnit.SECONDS)
.writeTimeout(90, TimeUnit.SECONDS)
.readTimeout(90, TimeUnit.SECONDS)
.build()
fun apiClient(): Retrofit {
if (retrofit == null) {
retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.client(okHttpClientvalor)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit!!
}
}
}
object ErrorUtils {
fun parseError(response: Response<*>): ErrorResponce? {
val conversorDeErro = ApiClient.apiClient()
.responseBodyConverter<ErrorResponce>(ErrorResponce::class.java, arrayOfNulls(0))
var errorResponce: ErrorResponce? = null
try {
if (response.errorBody() != null) {
errorResponce = conversorDeErro.convert(response.errorBody()!!)
}
} catch (e: IOException) {
return ErrorResponce()
} finally {
return errorResponce
}
}
}
class ErrorResponce {
/* This name "error" must match the message key returned by the server.
Example: {"error": "Bad Request ....."} */
#SerializedName("error")
#Expose
var error: String? = null
}
if (response.isSuccessful) {
return MyResponse(response.body() // transform
?: // some empty object)
} else {
val errorResponce = ErrorUtils.parseError(response)
errorResponce!!.error?.let { message ->
Toast.makeText(this,message,Toast.LENGTH_SHORT).show()
}
}
Retrofit defines successful response as such:
public boolean isSuccessful() {
return code >= 200 && code < 300; }
which means you should be able to do something like this
class ServiceImpl(private val myService: MyService) {
suspend fun startWork(//query): MyResponse =
myService.startWork(query).await().let {
if (response.isSuccessful) {
return MyResponse(response.body()//transform
?: //some empty object)
} else {
throw HttpException(response)//or handle - whatever
}
}
}
I am unable to get 400 response in error body of retrofit. I have set logging level its showing in logs but not showing in error body i have searched a lot but didn't find any solution is anyone there who help me in this case to get rid of this problem
call_.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>?, response: Response<ResponseBody>?) {
if (response?.code() == 400) {
var jObjError: JSONObject? = null
try {
var jObjErrorr = response.errorBody().string()
CustomLogs.displayLogs("$TAG jObjErrorr: $jObjErrorr")
} catch (e: Exception) {
}
try {
val string = jObjError?.getstring("error_description")
CustomLogs.displayLogs("$TAG jObjError: $string")
} catch (e: Exception) {
e.printStackTrace();
}
}
}
i need error body to get and display message and my log shows this
{"error":"Authorize","error_description":"Error in authentication"}
but error body is not showing this object
As IntelliJ Amiya mentioned in comment to your original post you should do this in onFailure method. As far as I know Retrofit's onResponse will not be called in cases of response code not in 200 range (200, 201, 202 etc.) so your check for if (response?.code() == 400) will never return true.
If you go through the Retrofit onResponse Library...,it's clearly mentioned that Retrofit does not create Body for response with status code below 200 or above 300.You have to specify your error Response Specifically!!
Decided to add it as separate answer:
if (response?.code() == 400) {
var jObjError: JSONObject? = null
try {
jObjError = response.errorBody().string()
CustomLogs.displayLogs("$TAG jObjError: $jObjError")
} catch (e: Exception) {
}
try {
val string = jObjError?.optString("error_description")
CustomLogs.displayLogs("$TAG jObjError: $string")
} catch (e: Exception) {
e.printStackTrace();
}
}
Could you try this fragment?
you can do this in Kotlin:
val errorResponse: ErrorMessage? = Gson().fromJson(
response.errorBody()!!.charStream(),
object : TypeToken<ErrorMessage>() {}.type
)
My current Android Application is employing Retrofit(2.4.0) and RxJava(2.1.16) to execute my Web Service calls.
Im using Google SignIn for my User Authentication.
I want my Retrofit calls to detect HTTP 401 (UNAUTHORIZED) and attempt to Silently Login with Google Signin
then retry the Retrofit call.
My retrofit calls resemble this
#Headers(HEADER_ACCEPT_JSON)
#GET("resources")
Observable<Response<String>> getResources(#Header(HEADER_AUTHORIZATION) #NonNull final String authenticationToken, #QueryMap(encoded = true) #NonNull Map<String, Object> queryMap);
API_SERVICE.getResources(Login.getAuthorizationToken(), id)
.subscribeOn(Schedulers.io())
.subscribe(Network::manageResource, Network::handle));
From googling I can see that retry/retryWhen will only be triggered when an error occurs in my RxJava chain,
however HTTP 401 errors are not going to raise this condition.
As a newbie to RxJava how can I detect my HTTP 401 code and..
a). Execute Google SignIn Silent login
b). Silent login completes OK, retry my API call?
UPDATE
Ive got closer with the following code
#Headers(HEADER_ACCEPT_JSON)
#GET("resources")
Single<Response<String>> getResources(#Header(HEADER_AUTHORIZATION) #NonNull final String authenticationToken, #QueryMap(encoded = true) #NonNull Map<String, Object> queryMap);
API_SERVICE.getResources(Login.getAuthorizationToken(), id)
.subscribeOn(Schedulers.io())
.flatMap(new Function<Response<Article>,
SingleSource<Response<Article>>>() {
#Override
public SingleSource<Response<Article>> apply(final Response<Article> response) {
Log.d(TAG, "apply() called with: response = [" + response + "]");
if (response.isSuccessful()) {
return Single.just(response);
} else {
return Single.error(new RuntimeException());
}
}
})
.retryWhen(errors -> errors.take(1).flatMap(new Function<Throwable, Publisher<?>>() {
#Override
public Publisher<?> apply(final Throwable throwable) {
Log.d(TAG, "apply() called with: throwable = [" + throwable + "]");
Login.loginSilently().subscribe();
return Flowable.just("DELAY").delay(10, TimeUnit.SECONDS);
}
}))
.subscribe(Network::manageResource, Network::handle));
I do not like the Flowable.just("DELAY").delay() call and also even though I am now catching the exception and silently login in OK I get this exception
09-10 16:39:29.878 7651-7718/research.android E/Network: handle:
java.util.NoSuchElementException
at io.reactivex.internal.operators.flowable.FlowableSingleSingle$SingleElementSubscriber.onComplete(FlowableSingleSingle.java:116)
at io.reactivex.subscribers.SerializedSubscriber.onComplete(SerializedSubscriber.java:168)
at io.reactivex.internal.operators.flowable.FlowableRepeatWhen$WhenReceiver.onComplete(FlowableRepeatWhen.java:119)
at io.reactivex.internal.operators.flowable.FlowableFlatMap$MergeSubscriber.drainLoop(FlowableFlatMap.java:426)
at io.reactivex.internal.operators.flowable.FlowableFlatMap$MergeSubscriber.drain(FlowableFlatMap.java:366)
at io.reactivex.internal.operators.flowable.FlowableFlatMap$InnerSubscriber.onComplete(FlowableFlatMap.java:673)
at io.reactivex.subscribers.SerializedSubscriber.onComplete(SerializedSubscriber.java:168)
at io.reactivex.internal.operators.flowable.FlowableDelay$DelaySubscriber$OnComplete.run(FlowableDelay.java:139)
at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
09-10 16:39:29.878 7651-7678/research.android D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled
How can I get the retrywhen to wait for the silentLogin to complete?
and
Whats causing the NoSuchElementException?
As far as I remember if you have error code > 300 then onError() will be called with Throwable which can ba cast to HttpException from where you can get error code returned by server so then you can call other function to make some "silent call"
When you initialize client:
Retrofit.Builder()
.baseUrl(baseUrl)
.client(createClient())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ApiHandler(Schedulers.io()))
.build()
Error handler:
class ApiHandler(scheduler: Scheduler) : CallAdapter.Factory() {
private val original: RxJava2CallAdapterFactory
= RxJava2CallAdapterFactory.createWithScheduler(scheduler)
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>?
= original.get(returnType, annotations, retrofit)?.let { Wrapper(it) }
private class Wrapper<R>(private val wrapped: CallAdapter<R, *>) : CallAdapter<R, Any> {
override fun adapt(call: Call<R>?): Any? {
call ?: return null
val result = wrapped.adapt(call)
return when (result) {
is Maybe<*> -> result.onErrorResumeNext(Function { Maybe.error(wrap(it)) })
is Single<*> -> result.onErrorResumeNext { Single.error(wrap(it)) }
is Completable -> result.onErrorResumeNext { Completable.error(wrap(it)) }
is Flowable<*> -> result.onErrorResumeNext(Function { Flowable.error(wrap(it)) })
is Observable<*> -> result.onErrorResumeNext(Function { Observable.error(wrap(it)) })
else -> result
}
}
override fun responseType(): Type = wrapped.responseType()
private fun wrap(throwable: Throwable) = when (throwable) {
is HttpException -> {
val exception = ApiException.http(throwable)
toLog("ex - ${exception.message}")
exception
} // We had non-200 http error
is JsonSyntaxException -> ApiException.parse(throwable) // We had json parsing error
is SocketTimeoutException -> ApiException.timeout(throwable) // A network error happened
is IOException -> ApiException.network(throwable) // A network error happened
else -> ApiException.unknown(throwable) // We don't know what happened. We need to simply convert to an unknown error
}
}
}
Api exception class:
class ApiException internal constructor(message: String,
/** Response object containing status code, headers, body, etc. */
val response: ErrorResponse?,
/** The event kind which triggered this error. */
#ApiError val error: Int,
exception: Throwable?) : RuntimeException(message, exception) {
companion object {
fun http(exception: HttpException): ApiException {
val response = exception.response()
var errorResponse: ErrorResponse? = null
val message = if (response == null) {
if (exception.message().isEmpty()) exception.code().toString() else exception.message()
} else {
// here you can check error code and throw needed exception
val errorBody = response.errorBody()?.string().toString()
if (errorBody.isNotEmpty()) {
toLog("ApiException: $errorBody")
}
try {
errorResponse = GsonBuilder().create().fromJson(errorBody, ErrorResponse::class.java)
errorResponse?.toString() ?: errorBody
} catch (e: Exception) {
e.printStackTrace()
response.raw().message()
}
}
return ApiException(message, errorResponse, ApiError.HTTP, exception)
}
fun network(exception: IOException): ApiException {
return ApiException(exception.message ?: "network", null, ApiError.NETWORK, exception)
}
fun parse(exception: JsonSyntaxException): ApiException {
return ApiException(exception.message ?: "parse", null, ApiError.CONVERSION, exception)
}
fun unknown(exception: Throwable): ApiException {
return ApiException(exception.message ?: "unknown", null, ApiError.UNKNOWN, exception)
}
fun timeout(exception: SocketTimeoutException): ApiException {
return ApiException("Connection timed out", null, ApiError.TIMEOUT_EXCEPTION, exception)
}
}
}
And when calling request
yourRequest.compose { observable ->
observable.retryWhen { flow ->
flow.ofType(ApiException::class.java).flatMap {
when {
it.error == ApiError.TIMEOUT_EXCEPTION -> Flowable.empty<T>()
it.error == ApiError.NETWORK -> getSnackBarFlowable().flatMap { if (it) Flowable.just(it) else Flowable.empty<T>() }
else -> Flowable.error(it)
}
}
}
}.subscribe({}, {})
getSnackBarFlowable() is get from fragment. you can use something else
fun getSnackBarFlowable(): Flowable<Boolean> = Flowable.create({ subscriber ->
if (view == null) {
subscriber.onNext(false)
} else {
val snackBar = Snackbar.make(activity!!.currentFocus, R.string.error_connection_fail, Snackbar.LENGTH_INDEFINITE)
snackBar.setAction("Retry") { subscriber.onNext(true) }
snackBar.show()
}
}, LATEST)
I know, quite enough of code. But this solution is really helpful for me in different projects
To solve 401 Unauthorized Error try to implement AuthInterceptor to your OkHttpClient.
BasicAuthInterceptor interceptorAuth = new BasicAuthInterceptor(yourToken);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(interceptorAuth)
.build();
builder.client(client);
If your authToken is expired or bad try to gain new.
public class BasicAuthInterceptor implements Interceptor {
private String yourToken;
public BasicAuthInterceptor(String token) {
this.yourToken = token;
}
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request authenticatedRequest = request.newBuilder()
.header("Authorization", format("token %s", yourToken)).build();
Response response = chain.proceed(authenticatedRequest);
boolean unauthorized = response.code() == 401;
if (unauthorized) {
Request modifiedRequest = request.newBuilder()
.header("Authorization", format("token %s", getNewToken())).build();
response = chain.proceed(modifiedRequest);
}
return response;
}
}