I'm using Retrofit2 in an Android app. Here's my response code:
override fun onResponse(call: Call<Asset>?, response: Response<Asset>?) {
val errorStart = response?.errorBody()?.string() // I get "ERROR" here
if (response != null && response.isSuccessful) {
// A successful response was returned
completion(response.body(), null)
} else {
// Not success
val errorElse = response?.errorBody()?.string() // I get "" here
val error = Error(errorText)
completion(null,error)
}
}
and here's the code I'm using to test my code:
response = Response.Builder()
.code(403)
.message(responseString)
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.body(ResponseBody.create(MediaType.parse("application/json"), "ERROR"))
.addHeader("content-type", "application/json")
.build()
When the code run and a response is returned response?.errorBody()?.string() is a string and I can capture it, as I have in this code as val errorStart.
...But...
when I run the IF code/logic and attempt to capture response?.errorBody()?.string() in the else code block it's now blank/gone.
Can someone explain what's going on or what I'm doing wrong? What is the proper way to capture error information in OnResponse?
First of all the errorBody() is of type stream, meaning that it is not read until you call the method. Streams can be read only once, take a look here for a detailed explanation.
Related
I have a retrofit Service:
suspend fun getArticles(): Articles
and usually I can get response code in try/catch in a case of error.
try {
val articles = service.getArticles()
} catch (e: Exception) {
// I can get only codes different from 200...
}
But what if I need to distinguish between 200 and 202 codes for example and my service returns me only data object?
How to get response code if I have successful response?
Your interface would look like this
suspend fun getArticles(): Response<Articles>
then you use like this
val response = service.getArticles()
response.code() //gives you response code
val articles = response.body
or easier solution is
if (response.isSuccessful){ // Successful is any code between the range [200..300)
val articles = response.body
}
I'm implementing Twitter OAuth flows as per:
https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter
I am getting a response back for the first step (oauth/request_token) which has a 200 code, but the response body is completely empty.
I'm using Retrofit to call the API, and have hooked up an interceptor OkHttpClient to debug the response like so:
val client = OkHttpClient.Builder().also { builder ->
builder.addInterceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
response
}
}.build()
Then setting up Retrofit like so:
Retrofit.Builder()
.baseUrl(TWITTER_AUTH_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
.create(TwitterAuthRetrofit::class.java)
.getRequestToken(
authorizationHeaders
).enqueue(object : Callback<TwitterRequestToken> {
override fun onResponse(call: Call<TwitterRequestToken>, response: Response<TwitterRequestToken>) {
onSuccess(response.body())
}
override fun onFailure(call: Call<TwitterRequestToken>, t: Throwable) {
onFailure()
}
})
When I debug in the interceptor, I can see the response is successful (200) but the response body is empty, which I think is causing my Gson deserialization to fail.
The result of calling response.body.contentLength() in the interceptor is -1.
The result of calling response.code in the interceptor is 200.
Here is the model I am attempting to deserialize the response body to:
data class TwitterRequestToken(
#SerializedName(value = "oauth_token")
val token: String,
#SerializedName(value = "oauth_token_secret")
val tokenSecret: String,
#SerializedName(value = "oauth_callback_confirmed")
val callbackConfirmed: Boolean
)
Note I am using #SerializedName to provide the keys for the response body, whilst the names of my properties are arbitrary to our app (we use camel case). I add a GsonConverterFactory to the Retrofit instance using the builder and have done this in the same way for many other requests before with no issues.
Here is the response I am getting from the API, which I am looking at via debugging in the interceptor above:
Response{protocol=h2, code=200, message=, url=https://api.twitter.com/oauth/request_token}
And here is the cause message from the Throwable I am getting in the onFailure callback from Retrofit:
com.google.gson.stream.MalformedJsonException:
Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $
Has anyone got any idea what might cause this?
Finally figured it out, hope this helps someone in future...
The response body from the Twitter API for oauth/request_token isn't encoded as JSON; you will need to read it from the response buffer. Specifically, when implementing the API with Retrofit, you will want your Retrofit interface to return ResponseBody (rather than your custom class), remove GSON from the Retrofit builder and, in the onResponseCallback from Retrofit, write the following code to read the buffer to a string, then split the string on & to get each key val pair, then you can split each of these on = and make sure you have all 3 values before constructing your model:
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
response.body()?.also { body ->
body.source().readString(Charsets.UTF_8).split('&').map { param ->
param.split('=').let { keyVal ->
keyVal[0] to keyVal[1]
}
}.toMap().let { paramMap ->
val oauthToken = paramMap["oauth_token"]
val oauthTokenSecret = paramMap["oauth_token_secret"]
val oauthCallbackConfirmed = paramMap["oauth_callback_confirmed"]?.toBoolean()
if (oauthToken == null || oauthTokenSecret == null || oauthCallbackConfirmed == null) {
onFailure()
} else {
onSuccess(
TwitterRequestToken(
oauthToken,
oauthTokenSecret,
oauthCallbackConfirmed
)
)
}
}
} ?: onFailure()
}
I am using two kind of interceptor, one is HttpLoggingInterceptor and another one is my custom AuthorizationInterceptor
I am using below updated retrofit version library,
def retrofit_version = "2.7.2"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
implementation 'com.squareup.okhttp3:okhttp:4.4.0'
below is code
private fun makeOkHttpClient(): OkHttpClient {
val logger = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
return OkHttpClient.Builder()
.addInterceptor(AuthorizationInterceptor(context)) <---- To put Authorization Barrier
.addInterceptor(logger) <---- To log Http request and response
.followRedirects(false)
.connectTimeout(50, TimeUnit.SECONDS)
.readTimeout(50, TimeUnit.SECONDS)
.writeTimeout(50, TimeUnit.SECONDS)
.build()
}
When I try to execute below code, in file named SynchronizationManager.kt, it gives me an error.
var rulesResourcesServices = RetrofitInstance(context).buildService(RulesResourcesServices::class.java)
val response = rulesResourcesServices.getConfigFile(file).execute() <---In this line I am getting an exception... (which is at SynchronizationManager.kt:185)
My RulesResourcesServices class is here
After debug I found that when below function called, at that time I am getting an exception
#GET("users/me/configfile")
fun getConfigFile(#Query("type") type: String): Call<ResponseBody>
I am getting following error
java.lang.IllegalStateException: closed
at okio.RealBufferedSource.read(RealBufferedSource.kt:184)
at okio.ForwardingSource.read(ForwardingSource.kt:29)
at retrofit2.OkHttpCall$ExceptionCatchingResponseBody$1.read(OkHttpCall.java:288)
at okio.RealBufferedSource.readAll(RealBufferedSource.kt:293)
at retrofit2.Utils.buffer(Utils.java:316)<------- ANDROID IS HIGH-LIGHTING
at retrofit2.BuiltInConverters$BufferingResponseBodyConverter.convert(BuiltInConverters.java:103)
at retrofit2.BuiltInConverters$BufferingResponseBodyConverter.convert(BuiltInConverters.java:96)
at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:225)
at retrofit2.OkHttpCall.execute(OkHttpCall.java:188)
at retrofit2.DefaultCallAdapterFactory$ExecutorCallbackCall.execute(DefaultCallAdapterFactory.java:97)
at android.onetap.SynchronizationManager.downloadFile(SynchronizationManager.kt:185)
at android.base.repository.LoginRepository.downloadConfigFilesAndLocalLogin(LoginRepository.kt:349)
at android.base.repository.LoginRepository.access$downloadConfigFilesAndLocalLogin(LoginRepository.kt:48)
at android.base.repository.LoginRepository$loginTask$2.onSRPLoginComplete(LoginRepository.kt:210)
at android.base.repository.LoginRepository$performSyncLogin$srpLogin$1$1.onSRPLogin(LoginRepository.kt:478)
at android.srp.SRPManager$SRPLoginOperation$execute$1.invokeSuspend(SRPManager.kt:323)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:561)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:727)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:667)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:655)
Below is screenshot, in that you can see that, I am getting output of file but don't know why it is throwing an exception.
checked Retrofit's Utils class
https://github.com/square/retrofit/blob/master/retrofit/src/main/java/retrofit2/Utils.java
static ResponseBody buffer(final ResponseBody body) throws IOException {
Buffer buffer = new Buffer();
body.source().readAll(buffer); <-This line throws an error.
return ResponseBody.create(body.contentType(), body.contentLength(), buffer);
}
Update
Same thing is working fine with enqueue method.
response.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(call: Call<ResponseBody?>, response: retrofit2.Response<ResponseBody?>) {
}
})
I have post same issue with Retrofit team, lets see.
https://github.com/square/retrofit/issues/3336
Thanks to JakeWharton (https://github.com/square/retrofit/issues/3336), I can be able to get solution.
Actually in my custom interceptor I was reading response by following code
Response.body().string()
I was doing because above code was helping me to find out that if there is any error than what kind of error it is....
if it is AUTH_ERROR, I have to generate new token and append it to request header.
According to retrofit document, if we call any of below method then response will be closed, which means it's not available to consume by the normal Retrofit internals.
Response.close()
Response.body().close()
Response.body().source().close()
Response.body().charStream().close()
Response.body().byteStream().close()
Response.body().bytes()
Response.body().string()
So to read data, I will use
response.peekBody(2048).string()
instead of
response.body().string(),
so it will not close response.
below is the final code
val response = chain.proceed(request)
val body = response.peekBody(Long.MAX_VALUE).string()//<---- Change
try {
if (response.isSuccessful) {
if (body.contains("status")) {
val jsonObject = JSONObject(body)
val status = jsonObject.optInt("status")
Timber.d("Status = $status")
if (status != null && status == 0) {
val errorCode = jsonObject.getJSONObject("data").optString("error_code")
if (errorCode != null) {
addRefreshTokenToRequest(request)
return chain.proceed(request)
}
}
} else {
Timber.d("Body is not containing status, might be not valid GSON")
}
}
Timber.d("End")
} catch (e: Exception) {
e.printStackTrace()
Timber.d("Error")
}
return response
Extending #Siddhpura Amit's answer:
If you don't know the bytes to pass into peak method then you can still use all of the methods, but will just have to create new Response object.
Inside interceptor:
okhttp3.Response response = chain.proceed(request);
String responseBodyString = response.body().string();
//Do whatever you want with the above string
ResponseBody body = ResponseBody.create(response.body().contentType(), responseBodyString);
return response.newBuilder().body(body).build();
maybe you closed your response in your AuthorizationInterceptor like this
override fun intercept(chain: Interceptor.Chain): Response {
...
val response = chain.proceed(builder.build())
response.close()
...
}
I am using the following method to handle my requests
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
.let { originalResponse ->
Log.i("AMIRA999", "code : " + originalResponse.code())
when (originalResponse.code()) {
200 -> {
Log.i("AMIRA999", "body : " + getErrorResponse(originalResponse))
originalResponse
}
401, 404 -> {
Log.i("AMIRA999", "body : " + getErrorResponse(originalResponse))
originalResponse
/*return originalResponse.mapToBody(
originalResponse.body()?.contentType(),
getErrorResponse(originalResponse)
)*/
}
else -> {
Log.i("AMIRA999", "body : " + originalResponse.body().toString())
throw BadRequestException()
}
}
}
the method work perfect when the code is 200, but it crash if the code is 404 or 401
what I need to keep returning the json comes from server and does not crash to be able to handle it with error message
how can I do that ?
the crash that I got is the following
retrofit2.HttpException: HTTP 401 UNAUTHORIZED
at com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory$BodyCallAdapter$adapt$2.onResponse(CoroutineCallAdapterFactory.kt:104)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:129)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:206)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
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)
You use retrofit2-kotlin-coroutines-adapter and the exception throwing is by design. Any non-2xx HTTP response such as 401 will throw an exception. You can see this for yourself in the library source code
if (response.isSuccessful) {
deferred.complete(response.body()!!)
} else {
deferred.completeExceptionally(HttpException(response))
}
But this is not a problem. You can still access the response and your JSON by doing catch (e: HttpException) and then calling val yourJson = e.response()?.body() as? YourJson.
Note that retrofit2-kotlin-coroutines-adapter is deprecated and that you should migrate to Retrofit 2.6.0 or newer. Then you can prefix your Retrofit interface functions with suspend so you can write nice idiomatic Kotlin code.
Retrofit 2 has a different concept of handling "successful" requests than Retrofit 1. In Retrofit 2, all requests that can be executed (sent to the API) and for which you’re receiving a response are seen as "successful". That means, for these requests the onResponse callback is fired and you need to manually check whether the request is actually successful (status 200-299) or erroneous (status 400-599).
If the request finished successfully, we can use the response object and do whatever we wanted. In case the error actually failed (remember, status 400-599), we want to show the user appropriate information about the issue.
Example
Error Object
Let’s assume your API sends a JSON error body like this:
{
statusCode: 409,
message: "Email address already registered"
}
Note: you can see your JSON error body by printing response.errorBody()
To avoid these bad user experiences, we’re mapping the response body to a Java object, represented by the following class.
class APIError {
private val statusCode:Int = 0
private val message:String
fun status():Int {
return statusCode
}
fun message():String {
return message
}
}
Error Handler
object ErrorUtils {
fun parseError(response:Response<*>):APIError {
val converter = ServiceGenerator.retrofit()
.responseBodyConverter(APIError::class.java, arrayOfNulls<Annotation>(0))
val error:APIError
try
{
error = converter.convert(response.errorBody())
}
catch (e:IOException) {
return APIError()
}
return error
}
}
Error Handler in Action
Now you can handle error in API response using ErrorUtils like the following.
val call = service.me()
call.enqueue(object:Callback<User>() {
fun onResponse(call:Call<User>, response:Response<User>) {
if (response.isSuccessful())
{
// use response data and do some fancy stuff :)
}
else
{
// parse the response body …
val error = ErrorUtils.parseError(response)
// … and use it to show error information
// … or just log the issue like we’re doing :)
Log.d("error message", error.message())
}
}
fun onFailure(call:Call<User>, t:Throwable) {
// there is more than just a failing request (like: no internet connection)
}
})
The complete example with a video is here retrofit-2-error-handling.
I'm using Retrofit to make a POST Request in my web server.
However, I can't seem to get the response body when the response status is 422 (unprocessable entity). The response body is always null.
I want to know if I'm doing something wrong or if there's a workaround for this. Because I'm using the same json in the request with Postman, and it returns the body normally.
This is the method:
#Headers("Content-Type: application/vnd.api+json")
#POST("my_endpoint")
Call<JsonObject> postEntry(#Header("Authorization") String authorization, #Body JsonObject json);
The body is a JsonObject, I'm not serializing like the documentation say. But I don't think this is the problem.
By default, when your server is returning an error code response.body() is always null. What you are looking for is response.errorBody(). A common approach would be something like this:
#Override
public void onResponse(Response<JsonObject> response, Retrofit retrofit) {
if (response.isSuccess()) {
response.body(); // do something with this
} else {
response.errorBody(); // do something with that
}
}
If you need something advanced take a look at Interceptors and how to use them
I got the same error. My API was working using POSTMAN request but not working from Android retrofit call.
At first I was trying using #Field but it was getting error but later I've tried with #Body and it worked.
Sample Retrofit interface call
#POST("api/v1/app/instance")
Call<InstanceResponse> updateTokenValue(
#HeaderMap Map<String, String> headers,
#Body String body);
and API calling code is:
Map<String, String> headerMap=new HashMap<>();
headerMap.put("Accept", "application/json");
headerMap.put("Content-Type", "application/json");
headerMap.put("X-Authorization","access_token");
Map<String, String> fields = new HashMap<>();
fields.put("app_name", "video");
fields.put("app_version", "2.0.0");
fields.put("firebase_token", "token");
fields.put("primary", "1");
ApiInterface apiInterface = ApiClient.getApiClient().create(ApiInterface.class);
Call<InstanceResponse> call = apiInterface.updateTokenValue(
headerMap,new Gson().toJson(fields));
Well in this case you'll have to convert the response.
Have a look at this link
All the steps are already provided in the link above.
For Kotlin users here is the code solution.
ErrorResponse.kt (This obviously depends on your error response)
import com.squareup.moshi.Json
data class ErrorResponse(
#Json(name="name")
val name: String? = null,
#Json(name="message")
val message: String? = null,
#Json(name="errors")
val errors: Errors? = null,
#Json(name="statusCode")
val statusCode: Int? = null
)
ApiFactory.kt (Let me know if you need the entire code)
fun parseError(response: Response<*>): ErrorResponse {
val converter = ApiFactory.retrofit()
.responseBodyConverter<ErrorResponse>(
ErrorResponse::class.java, arrayOfNulls<Annotation>(0)
)
val error: ErrorResponse
try {
error = converter.convert(response.errorBody()!!)!!
} catch (e: IOException) {
e.printStackTrace()
return ErrorResponse()
}
return error
}
and in the Presenter (I use MVP)
GlobalScope.launch(Dispatchers.Main) {
try {
val response = ApiFactory.apiService.LOGIN(username, password)
.await()
val body = response.body()
body?.let {
// Handle success or any other stuff
if (it.statusCode == 200) {
mView.onSuccess(it.data!!)
}
} ?:
// This is the else part where your body is null
// Here is how you use it.
// Pass the response for error handling
mView.showMessage(ApiFactory.parseError(response).message!!)
} catch (e: Exception) {
e.printStackTrace()
}
}
And thats how you roll it!
That's All Folks!