Retrofit 2如何处理登录请求中时而返回空响应体、时而返回非空响应体的场景
Hey there, let's tackle this tricky Retrofit scenario you're facing! I've run into similar issues before, so here are a few solid solutions that should work for you:
ResponseBody as the Return Type (Simplest Approach) This is the most straightforward fix for your specific login endpoint. By using ResponseBody, you can handle both empty success responses and error responses with custom parsing:
First, update your API interface:
interface LoginAPIService { @POST("login") fun login(@Body body: RequestBody): Call<ResponseBody> }
Then, handle the response in your callback:
loginApiService.login(yourRequestBody).enqueue(object : Callback<ResponseBody> { override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) { when { response.isSuccessful -> { // Success case: empty response body, proceed with login success logic showLoginSuccess() } else -> { // Failure case: parse the error message from error body response.errorBody()?.let { errorBody -> try { // Use your JSON parser (Gson/Moshi/Jackson) to deserialize the error val errorResponse = Gson().fromJson( errorBody.string(), ErrorResponse::class.java ) showLoginError(errorResponse.message) } catch (e: Exception) { // Fallback if parsing fails showLoginError("Unknown error occurred") } } ?: showLoginError("No error message available") } } } override fun onFailure(call: Call<ResponseBody>, t: Throwable) { // Handle network errors (e.g., no internet, timeout) showLoginError(t.message ?: "Network error") } }) // Define a data class to map the error response data class ErrorResponse(val message: String)
Pros: No Retrofit configuration changes needed, perfect for one-off endpoints like login.
Cons: Requires manual parsing of the error body each time you use this pattern.
If you have multiple endpoints with similar behavior (empty success, error with message), a custom converter will let you handle this uniformly across your app.
First, define a sealed class to represent your API responses:
sealed class ApiResult { object Success : ApiResult() data class Error(val message: String) : ApiResult() }
Then, create a custom converter factory that handles empty success bodies and error parsing:
class EmptyBodyConverterFactory(private val gson: Gson) : Converter.Factory() { override fun responseBodyConverter( type: Type, annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { // Only apply this converter if the target type is ApiResult return if (type == ApiResult::class.java) { Converter<ResponseBody, ApiResult> { body -> if (body.contentLength() == 0L) { ApiResult.Success } else { // This should only happen for error responses, but add a fallback try { val errorResponse = gson.fromJson(body.string(), ErrorResponse::class.java) ApiResult.Error(errorResponse.message) } catch (e: Exception) { ApiResult.Error("Unknown error") } } } } else { // Delegate to other converters for other types super.responseBodyConverter(type, annotations, retrofit) } } }
Add this converter to your Retrofit instance:
val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(EmptyBodyConverterFactory(Gson())) .addConverterFactory(GsonConverterFactory.create()) .build()
Update your API interface to use ApiResult:
interface LoginAPIService { @POST("login") fun login(@Body body: RequestBody): Call<ApiResult> }
Now handling the response becomes cleaner:
loginApiService.login(yourRequestBody).enqueue(object : Callback<ApiResult> { override fun onResponse(call: Call<ApiResult>, response: Response<ApiResult>) { when (val result = response.body()) { is ApiResult.Success -> showLoginSuccess() is ApiResult.Error -> showLoginError(result.message) null -> { // Handle cases where response body is null but request wasn't successful response.errorBody()?.let { val errorMsg = Gson().fromJson(it.string(), ErrorResponse::class.java).message showLoginError(errorMsg) } ?: showLoginError("Unknown error") } } } override fun onFailure(call: Call<ApiResult>, t: Throwable) { showLoginError(t.message ?: "Network error") } })
Pros: Uniform handling across all endpoints with this pattern, less repetitive code.
Cons: Requires setting up a custom converter, which adds a bit of initial overhead.
If you're using Retrofit with Kotlin coroutines, you can simplify the handling even more:
interface LoginAPIService { @POST("login") suspend fun login(@Body body: RequestBody): Response<ResponseBody> } // In your repository/viewmodel try { val response = loginApiService.login(yourRequestBody) if (response.isSuccessful) { showLoginSuccess() } else { response.errorBody()?.let { val errorMsg = Gson().fromJson(it.string(), ErrorResponse::class.java).message showLoginError(errorMsg) } ?: showLoginError("Unknown error") } } catch (e: Exception) { showLoginError(e.message ?: "Network error") }
This avoids callback boilerplate and makes the code more readable.
内容的提问来源于stack exchange,提问作者Alex Petev

