Kotlin and Retrofit : How to Handle HTTP 400 responses? - android

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
}
}
}

Related

Refresh access token using authenticatior in retrofit android

How can I refresh my token using authenticator? I need the refresh token method to return the token or null when I get 401 in my api call.
class SupportInterceptor() : Interceptor, Authenticator {
/**
* Interceptor class for setting of the headers for every request
*/
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
request = request?.newBuilder()
?.addHeader("Content-Type", "application/json")
?.addHeader("app-id", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
?.build()
return chain.proceed(request)
}
/**
* Returns a request that includes a credential to satisfy an authentication challenge in
* [response]. Returns null if the challenge cannot be satisfied.
*
* The route is best effort, it currently may not always be provided even when logically
* available. It may also not be provided when an authenticator is re-used manually in an
* application interceptor, such as when implementing client-specific retries.
*/
override fun authenticate(route: Route?, response: Response): Request? {
var requestAvailable: Request? = null
try {
return runBlocking {
when (val tokenResponse = refreshToken()) {
is Success -> {
// userPreferences.saveAccessTokens(
// tokenResponse.value.access_token!!,
// tokenResponse.value.refresh_token!!
// )
response.request.newBuilder()
.header("Authorization", "Bearer ${tokenResponse.value.access_token}")
.build()
}
else -> null
}
}
// requestAvailable = response?.request?.newBuilder()
//// ?.addHeader("Authorization", "Bearer $token")
// ?.build()
// return requestAvailable
} catch (ex: Exception) {
}
return requestAvailable
}
suspend fun refreshToken(): Either<Failure, String?> {
return withContext(Dispatchers.IO) {
try {
val PREFS_NAME = "userPref"
val sharedPref: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val refreshToken = sharedPref.getString(MyConstants.KEY_REFRESH_TOKEN, "")
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(baseURL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val api: TokenRefreshApi = retrofit.create(TokenRefreshApi::class.java)
val response = api.refreshAccessToken(refreshToken).execute()
// val call: Call<LogIn> = api.refreshAccessToken(refreshToken)
when (response.isSuccessful) {
false -> Either.Left(response.errorResponse())
true -> {
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putString(
MyConstants.KEY_ACCESS_TOKEN,
response.body()!!.access_token
)
editor.putString(
MyConstants.KEY_REFRESH_TOKEN,
response.body()!!.refresh_token
)
editor!!.apply()
response.body()!!.access_token
}
}
} catch (e: Exception) {
Timber.e("searchTasks: $e")
Either.Left(Failure.UnknownError)
}
}
}
}
I would first clarify the industry standard behavior for reliable clients:
Client tries an API request with an access token
If client receives a 401 it attempts to silently refresh the access token and retry the API request with the new token
If there are technical problems avoid redirecting the user to re-authenticate
Here is some plain Kotlin code of mine that does this.
Looks to me like Retrofit's Authenticator interface makes this easier, and will do the retry for you. Your code looks mostly correct and similar to mine but without the manual checks for 401s:
You need to test 401s though, and one way to do this is to add arbitrary characters to an access token during development, to simulate expiry
In case someone is having difficulty same like me.
override fun authenticate(route: Route?, response: Response): Request? {
var requestAvailable: Request? = null
try {
return runBlocking {
val tokenResponse = getNewToken()
if (!tokenResponse.isNullOrEmpty()) {
response.request.newBuilder()
.header("Authorization", "Bearer ${tokenResponse}")
.build()
} else {
null
}
}
} catch (ex: Exception) {
}
return requestAvailable
}
private fun getNewToken(): String? {
val PREFS_NAME = "userPref"
val sharedPref: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val refreshToken: String = sharedPref.getString("refresh_token", "").orEmpty()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_API_URL_CONSUMER)
.addConverterFactory(GsonConverterFactory.create())
.build()
val hashMap = HashMap<String, String>()
hashMap.put("refresh_token", refreshToken)
val call = retrofit.create(TokenRefreshApi::class.java).refreshAccessToken(hashMap)
val authTokenResponse = call?.execute()?.body()
if (authTokenResponse != null) {
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putString(
"access_token",
authTokenResponse!!.access_token
)
editor.putString(
"refresh_token",
authTokenResponse!!.refresh_token
)
editor!!.apply()
return authTokenResponse!!.access_token
} else {
return null
}
}

What is the best practice for parsing retrofit response on android with coroutines

I am trying to figure out what is the best practice for handling a retrofit response.
I provide the retrofit singlton like this:
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.addInterceptor(httpLoggingInterceptor)
.build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())
.build()
val service = retrofit.create(ShowsService::class.java)
The service interface is this:
interface ShowsService {
#GET("popular")
suspend fun fetchPopularShows(): Response<PopularShows>
}
I get a list of shows from API and parse it in a repository like this:
override suspend fun getShows(): Result<List<Show>?> {
val shows = service.fetchPopularShows()
val body = shows.body()
val errorBody = shows.errorBody()
return when {
body != null -> {
Result.Success(body.shows)
}
errorBody != null -> {
Result.Error(Exception(errorBody.string()))
}
else -> {
Result.Error(Exception("Unknown error: ${shows.raw().message}"))
}
}
}
However, this feels very non-kotlin and also would probably result in code duplication eventually, can anyone point me to a sample where this is implemented in the best practice?
In principle, you could create an unwrapResponse() generic function that takes a Response<T> and returns a Result<T?> and incorporates your algorithm. By eyeball, something like this:
suspend fun <T> unwrapResponse(response: Response<T>): Result<T> {
val body = response.body()
val errorBody = response.errorBody()
return when {
body != null -> {
Result.Success(body)
}
errorBody != null -> {
Result.Error(Exception(errorBody.string()))
}
else -> {
Result.Error(Exception("Unknown error: ${response.raw().message}"))
}
}
}
You could then call unwrapResponse(service.fetchPopularShows()) to get a Result<PopularShows>.
If you really wanted to allow unwrapResponse() to return a Result<List<Show>?>, you would wind up with something like:
suspend fun <T, R> unwrapResponse(response: Response<T>, unpacker: (T) -> R?): Result<R?> {
val body = response.body()
val errorBody = response.errorBody()
return when {
body != null -> {
Result.Success(unpacker(body))
}
errorBody != null -> {
Result.Error(Exception(errorBody.string()))
}
else -> {
Result.Error(Exception("Unknown error: ${response.raw().message}"))
}
}
}
unwrapResponse(service.fetchPopularShows()) { it.shows } would then give your Result<List<Show>?>.
Again, this is all by eyeball — adjustments are likely to be needed here.

400 error after successfully refreshing Oauth 2 Token using Retrofit2 / Kotlin in Android

This is the code I have for the refreshing token in an android app using kotlin ad retrofit 2.
The gradle :
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:retrofit:2.9.0"
And Authenticator is :
class OAuthAuthenticator(
private val refreshTokenService: Repository,
private val sessionManager: SessionManager
) : Authenticator {
#Synchronized
override fun authenticate(route: Route?, response: Response): Request? {
try {
//synchronized call to refresh the token
val refreshTokenResponse =
refreshTokenService.refreshJWTToken(sessionManager.getAuthTokens())
val sessionDataResponseBody = refreshTokenResponse.body()
if (refreshTokenResponse.isSuccessful && sessionDataResponseBody != null && !sessionDataResponseBody.jwt.isNullOrEmpty()) {
sessionManager.jwtToken = sessionDataResponseBody.jwt
// retry request with the new tokens (I get 400 error)
return response.request()
.newBuilder()
.addHeader("Authorization", "Bearer ${sessionManager.jwtToken}")
.build()
} else {
throw HttpException(refreshTokenResponse)
}
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
onSessionExpiration()
return null
}
}
}
return null
}
private fun onSessionExpiration() {
sessionManager.clear()
}
}
This is the Repository class :
object Repository {
fun refreshJWTToken(authTokens : AuthTokens) = RetrofitBuilder.userApi.getAuthenticationToken(authTokens).execute()
}
This is the API :
interface UserAPI {
#Headers("Cache-Control: no-cache")
#POST(AUTH_TOKENS_URL)
fun getAuthenticationToken(
#Header("Accept") accept : String,
#Header("Content-Type") contentType : String,
#Body params: AuthTokens
): Call<AuthTokenResponse>
}
The retrofit builder:
init {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val sessionManager = SessionManager.getInstance(context)
val httpLoggingInterceptor =
HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
httpClient = OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(ConnectivityCheckInterceptor(connectivityManager))
.addInterceptor(AuthInterceptor(sessionManager))
.authenticator(OAuth2Authenticator(UserRepository, sessionManager))
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.build()
}
Question :
I can confirm that the code refreshes the Auth token and persists it successfully. However I get a 400 error after that. Any suggestions on what I am doing wrong?
I know this question is old, but for everyone who facing the same issue, it was just a simple mistake.
Please, use header(..., ...) instead of addHeader(..., ...) in the TokenAuthenticator class.
It worked for me.

Call Api Again After Token Refresh

I am using Authenticator instead of Interceptor to refresh the token. I am able to detect the 401 exception and easily refresh the new token. Everything is working perfectly but the issue is following:
I am unable to call the request again, I do not want the user to hit again to call the offer.
So after execution of the code below I get a new token, it gives me a 401 error message.
My Question is: How can I call the request chain again?
Any advice on the implementation is welcome.
class OffersViewModel
val observable = ApiServiceClient.createApiUsingToken(context).getOffers(
Pref.getString(getApplication(), Pref.CUSTOMER_CODE, "")!!,
Pref.getString(getApplication(), Pref.TOKEN, "")!!
)
compositeDisposable.add(observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
responseModel.statusCode = StatusCode.START
offersRegisteredUserResponseLiveData.postValue(responseModel)
}
.subscribe({ success ->
if (success.errors.isNullOrEmpty()) {
success.statusCode = StatusCode.SUCCESS
} else {
success.statusCode = StatusCode.ERROR
}
offersRegisteredUserResponseLiveData.value = success
}, {
//HERE I GOT 401
Log.d("debug",it.message.toString())
responseModel.statusCode = StatusCode.ERROR
offersRegisteredUserResponseLiveData.value = responseModel
}, { })
)
API Service Class
/*.....Offer Screen...........*/
#GET("offers/xyz/{abc}")
fun getOffers(
#Path("abc") customerCode: String,
#Header("Authorization") authorization: String,
#Header("Content-Type") contentType: String = CONTENT_TYPE
):
Observable<OfferRegisteredUserResponseModel>
ApiClient Class
fun createApiUsingToken(context: Context?): ApiService {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder().addInterceptor(interceptor).connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.authenticator(TokenInterceptor(context)).build()
val retrofit = Retrofit.Builder()
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Constants.BASE_URL)
.build()
var ApiServiceClient=retrofit.create(ApiService::class.java)
return retrofit.create(ApiService::class.java)
}
class TokenInterceptor
var requestAvailable: Request? = null
if (response!!.code() === 401) {
var retrofitResponse = ApiServiceClient.createToken().getTokenWithoutObserver().execute()
if (retrofitResponse != null) {
val refreshTokenResponse = retrofitResponse!!.body()
val newAccessToken = refreshTokenResponse!!.token
if (newAccessToken != null)
{
Pref.setString(MyApplication.mInstance, Pref.TOKEN, "${refreshTokenResponse.tokenType} ${refreshTokenResponse?.token}")
Pref.setString(MyApplication.mInstance, Pref.TOKEN_EXPIRES_IN, refreshTokenResponse.tokenExpirationTime.toString())
Utils.addTokenExpirationTimeToCurrentTime(MyApplication.mInstance, refreshTokenResponse.tokenExpirationTime?.toInt()!!)
try {
requestAvailable = response?.request()?.newBuilder()
?.addHeader("Content-Type", "application/json")
?.addHeader("Authorization", "Bearer " + newAccessToken)
?.build()
return requestAvailable
} catch (ex: Exception) {
}
}
} else
return null
}
return requestAvailable
Couple of things i see wrong with this.
First is that even if you "restart" the request with the new token, if you happen to make another request while the "new token" is not saved, that request is also going to fail.
Second is that i don't see that you save the new token anywhere (in SharedPrefs for example for later use).
This is how i would have do it: (preferenceHelper is SharedPrefs)
override fun authenticate(route: Route?, response: Response): Request? {
val HEADER_AUTHORIZATION = "Authorization"
// We need to have a token in order to refresh it.
val token = preferenceHelper.getAccessToken() ?: return null
synchronized(this) {
val newToken = preferenceHelper.getAccessToken() ?: return null
// Check if the request made was previously made as an authenticated request.
if (response.request().header(HEADER_AUTHORIZATION) != null) {
// If the token has changed since the request was made, use the new token.
if (newToken != token) {
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + newToken)
.build()
}
val tokenResponse = ApiServiceClient.createToken().getTokenWithoutObserver().execute()
if (tokenResponse.isSuccessful) {
val userToken = tokenResponse.body() ?: return null
preferenceHelper.saveAccessToken(userToken.token)
preferenceHelper.saveRefreshToken(userToken.refreshToken)
// Retry the request with the new token.
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + userToken.token)
.build()
} else {
logoutUser()
}
}
}
return null
}

OkHttp token interceptor retries forever

I'm making an OkHttp interceptor so it retry to sign in when any end point returns a 401 error, but the interceptor is looping forever.
I've also tried to add a counter, but the counter resets itself every time.
Here's my code:
object Service {
private fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(getLoggingInterceptor())
.addInterceptor(NetworkInterceptor())
.build()
}
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(getOkHttpClient())
.baseUrl(getBaseUrl())
.build()
}
}
class NetworkInterceptor: Interceptor {
var counter = 0
override fun intercept(chain: Interceptor.Chain): Response? {
val originalResponse = chain.proceed(chain.request())
if (!originalResponse.isSuccessful && originalResponse.code() == 401) {
Log.e("NetworkInterceptor", "Network error 401. Counter = $counter")
counter++
val refreshedToken = refreshToken()
Log.e("NetworkInterceptor", "refreshedToken = $refreshedToken")
}
return originalResponse
}
private fun refreshToken(): String {
val context = MyApp.appContext
val preferencesUtil = SharePreferencesUtils(context)
val username = preferencesUtil.getUsername()
val password = preferencesUtil.getPassword()
val login = AuthService().loginSync(username, password).execute()
return login.body()?.access_token!!
}
}
I have tried this with an Auth call being an RxJava Single, and a regular synchronous Call<>
In each case, the call happens forever, the 401 gets returned forever, and the counter always stays at 0.
Any ideas on what I'm missing or doing wrong?
Thank you very much!
Add as a NetworkInterceptor using
addNetworkInterceptor(NetworkInterceptor)
refer to this link for further clarification: https://github.com/square/okhttp/wiki/Interceptors

Categories

Resources