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

根据要求添加 ApiResponseWrapperLoginRemote

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.

观察到