Reputation: 792
We are using retrofit to make API requests in our Android app and are running into a weird issue. I have a request that looks like this:
interface apiservice {
@POST("user/login")
fun postLogin(@Body body: LoginBody): LiveData<ApiResponseWrapper<LoginRemote>>
I make the request by doing:
apiservice.postLogin(
....
)
However, the first time this get's submitted, the request url is:
http://localhost/
First off, there isn't a single configuration in my app that sets the base url as localhost. Secondly, this request doesn't even have the /user/login
appended to it.
This was working fine the last time we worked on this app a few weeks ago, but now it has this weird bug when we didn't even touch anything related to this...
EDIT
Here is my Retrofit builder
Retrofit.Builder()
.baseUrl("${BuildConfig.API_URL}/en/api/v1/")
.client(makeOkHttpClient(BuildConfig.DEBUG, sessionData))
.addCallAdapterFactory(LiveDataCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(YTPService::class.java)
Here is my Build Config in my :
buildConfigField("String", "API_URL", "\"https://myurl.org\"")
// BuildConfig's `DEBUG` gets set to FALSE when build occurs
And then this is what is printing out in the log files during this process:
// THE FIRST TIME I HIT SUBMIT AND TRIGGER THE REQUEST
D/LOGIN-RESPONSE: Response{protocol=http/1.1, code=500, message=Response.error(), url=http://localhost/}
Error(errors=[com.mycompany.myproject.api.auth.login.LoginResponse$ErrorType$Generic@2148520])
...
// THE SECOND TIME I HIT SUBMIT AND TRIGGER THE REQUEST IMMEDIATELY AFTER FIRST FAILED
D/LOGIN-RESPONSE: Response{protocol=http/1.1, code=200, message=Logged in as SampleUser1., url=https://myurl.org/en/api/v1/user/login}
Ok(data=com.mycompany.myproject.api.auth.login.LoginResponse$SuccessData@4f03c75)
For even more details, check out this issue I opened on their repo
UPDATE
After looking at the logs on the server, I have confirmed that the proper request is actually being made, but retrofit's ApiResponseWrapper isn't setting the response with the proper information sent back from the server.
e.g.
apiservice.postLogin(LoginBody(username, password)).map { // it: ApiResponseWrapper<LoginRemote>
// `it.response` == Response{protocol=http/1.1, code=500, message=Response.error(), url=http://localhost/}
// When, in fact, the server actually sent back a 200 with the data I need. Retrofit just isn't handling it correct...
}
EDIT 2
Adding ApiResponseWrapper
and LoginRemote
classes as requested
ApiResponseWrapper
sealed class ApiResponseWrapper<R> {
companion object {
fun <R> success(response: Response<R>): ApiResponseWrapper.Success<R> =
ApiResponseWrapper.Success(response)
fun <T> failure(error: Throwable): ApiResponseWrapper.Failure<T> =
ApiResponseWrapper.Failure(error)
}
class Failure<T>(val error: Throwable) : ApiResponseWrapper<T>()
class Success<T>(val response: Response<T>) : ApiResponseWrapper<T>()
}
LoginRemote
class LoginRemote(@SerializedName("user") val user: User) {
companion object {
fun parseResponse(response: Response<LoginRemote>): LoginResponse {
return if (response.isSuccessful) {
Log.d("LOGIN-RESPONSE-BODY", response.body().toString())
response.body()!!.format()
} else if (response.code() == 302) {
// accept terms again
LoginResponse(listOf(LoginResponse.ErrorType.TermsRequired()))
} else {
val errorBody = response.errorBody()?.string()?.trim() ?: ""
if (errorBody == "[\"Wrong username or password.\"]" || errorBody.contains("has not been activated or is blocked."))
return LoginResponse(listOf(LoginResponse.ErrorType.CredentialsInvalid()))
if (errorBody.contains("[\"Already logged in as "))
return LoginResponse(LoginResponse.SuccessData(null, null, null))
return LoginResponse(listOf(LoginResponse.ErrorType.Generic()))
}
}
}
fun format(): LoginResponse {
Log.d("LOGIN-ROLE-KEYS", user.roles.toString())
val roles = user.roles.keys
val role = when {
roles.contains(ROLE_ID_STANDARD) -> Role(ROLE_ID_STANDARD)
roles.contains(ROLE_ID_LIMITED) -> Role(ROLE_ID_LIMITED)
else -> Role(ROLE_ID_PREMIUM)
}
val countryOfResidence = if (user.countryOfResidence != null) user.countryOfResidence!!.und[0].value else "United Arab Emirates"
return LoginResponse(LoginResponse.SuccessData(role = role, gender = user.fieldGender, countryOfResidence = countryOfResidence))
}
data class User(
@SerializedName("field_gender") val fieldGender: Gender?,
@SerializedName("roles") val roles: Map<Int, String>,
@SerializedName("field_new_residence") val countryOfResidence: CountryOfResidence?
)
data class CountryOfResidence(
@SerializedName("und") val und: List<CountryOfResidenceUND>
)
data class CountryOfResidenceUND(
@SerializedName("value") val value: String
)
}
Upvotes: 0
Views: 628
Reputation: 6607
Even though I cannot provide a full answer because I am not entirely sure on how everything is implemented, the problem seems to be related to the use of LiveData
as the return type in your Retrofit
service. Even though apparently you can do it by creating your custom Adapter
as shown in this Medium post, you shouldn't be using an Android Lifecycle aware component so far down in the architecture. Retrofit shouldn't know anything about Android. Moreover, it doesn't make sense because LiveData
it's supposed to expose a value that can change over time, whereas this would be a one-shot use (check this discussion)
My suggestion is that you change your API service and use a RxJava
observable such as Single
instead. For that, you will have to use this Adapter. Everytime you call the endpoint, you will get a response that can be then manually set into a LiveData
component. In turn, that LiveData
will be observed by your Activity.
Upvotes: 1