Android Retrofit 尝试在第一次尝试时发出错误的请求
Android Retrofit attempting to make incorrect request on first attempt
我们正在使用改造在我们的 Android 应用程序中发出 API 请求,并且 运行 遇到了一个奇怪的问题。我有一个看起来像这样的请求:
interface apiservice {
@POST("user/login")
fun postLogin(@Body body: LoginBody): LiveData<ApiResponseWrapper<LoginRemote>>
我通过以下方式提出请求:
apiservice.postLogin(
....
)
然而,第一次提交这个 get 时,请求 url 是:
http://localhost/
首先,我的应用程序中没有一个配置将基础 url 设置为本地主机。其次,这个请求甚至没有附加 /user/login
。
几周前我们最后一次开发这个应用程序时它运行良好,但现在当我们甚至没有触及与此相关的任何内容时它出现了这个奇怪的错误...
编辑
这是我的 Retrofit 构建器
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)
这是我的构建配置:
buildConfigField("String", "API_URL", "\"https://myurl.org\"")
// BuildConfig's `DEBUG` gets set to FALSE when build occurs
然后这是在此过程中在日志文件中打印出的内容:
// 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)
有关更多详细信息,请查看我在他们的存储库中打开的 issue
更新
查看服务器上的日志后,我确认确实发出了正确的请求,但改造的 ApiResponseWrapper 没有使用从服务器发回的正确信息设置响应。
例如
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...
}
编辑 2
根据要求添加 ApiResponseWrapper
和 LoginRemote
类
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>()
}
登录远程
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
)
}
虽然我不能提供完整的答案,因为我不完全确定一切是如何实现的,但问题似乎与使用 LiveData
作为 return 类型有关Retrofit
服务。尽管显然您可以通过创建自定义 Adapter
来做到这一点,如图 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)
我的建议是更改 API 服务并改用 RxJava
可观察对象,例如 Single
。为此,您将不得不使用 this Adapter。每次调用端点时,您都会收到一个响应,然后可以将其手动设置到 LiveData
组件中。反过来,LiveData
将被您的 Activity.
观察到
我们正在使用改造在我们的 Android 应用程序中发出 API 请求,并且 运行 遇到了一个奇怪的问题。我有一个看起来像这样的请求:
interface apiservice {
@POST("user/login")
fun postLogin(@Body body: LoginBody): LiveData<ApiResponseWrapper<LoginRemote>>
我通过以下方式提出请求:
apiservice.postLogin(
....
)
然而,第一次提交这个 get 时,请求 url 是:
http://localhost/
首先,我的应用程序中没有一个配置将基础 url 设置为本地主机。其次,这个请求甚至没有附加 /user/login
。
几周前我们最后一次开发这个应用程序时它运行良好,但现在当我们甚至没有触及与此相关的任何内容时它出现了这个奇怪的错误...
编辑
这是我的 Retrofit 构建器
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)
这是我的构建配置:
buildConfigField("String", "API_URL", "\"https://myurl.org\"")
// BuildConfig's `DEBUG` gets set to FALSE when build occurs
然后这是在此过程中在日志文件中打印出的内容:
// 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)
有关更多详细信息,请查看我在他们的存储库中打开的 issue
更新
查看服务器上的日志后,我确认确实发出了正确的请求,但改造的 ApiResponseWrapper 没有使用从服务器发回的正确信息设置响应。
例如
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...
}
编辑 2
根据要求添加 ApiResponseWrapper
和 LoginRemote
类
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>()
}
登录远程
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
)
}
虽然我不能提供完整的答案,因为我不完全确定一切是如何实现的,但问题似乎与使用 LiveData
作为 return 类型有关Retrofit
服务。尽管显然您可以通过创建自定义 Adapter
来做到这一点,如图 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)
我的建议是更改 API 服务并改用 RxJava
可观察对象,例如 Single
。为此,您将不得不使用 this Adapter。每次调用端点时,您都会收到一个响应,然后可以将其手动设置到 LiveData
组件中。反过来,LiveData
将被您的 Activity.