Kotlin中Retrofit实现带JWT返回的POST登录接口问题
Alright, let's work through this Retrofit + Kotlin login challenge you're hitting up against. From what you shared, the issue is almost certainly tied to how Retrofit handles different HTTP status codes and their response bodies—since your API returns similar but distinct payloads for 200 (success with token) and 403 (error with null token and detailed code). Here's how to fix it step by step:
1. Simplify Your DTOs (Use a Single Response Model)
Instead of two separate DTOs, create one unified LoginResponse that accounts for both success and error cases. Since the only difference is whether token is present or null, making that field nullable covers both scenarios:
data class LoginResponse( val token: String?, // Nullable for 403 responses val code: String // Contains success code or error details ) // Your credentials DTO stays the same data class LoginCredentials( val username: String, val password: String )
2. Update Your Retrofit Service Interface
By default, Retrofit throws an HttpException for non-2xx status codes (like 403). To avoid this and handle responses manually, have your service return a Response<LoginResponse> instead of the raw DTO:
interface AuthService { @POST("login") // Replace with your actual endpoint path suspend fun login(@Body credentials: LoginCredentials): Response<LoginResponse> }
3. Handle Responses in Your Login Logic
Now you can explicitly check the status code and parse the response body (or error body) accordingly. Here's how to implement this in a suspend function:
suspend fun performLogin(username: String, password: String) { val authService = Retrofit.Builder() .baseUrl("YOUR_BASE_URL") // Add your base URL .addConverterFactory(GsonConverterFactory.create()) // Or Moshi, etc. .build() .create(AuthService::class.java) val credentials = LoginCredentials(username, password) val response = authService.login(credentials) when { response.isSuccessful -> { // Handle 200 OK response response.body()?.let { loginResponse -> loginResponse.token?.let { jwtToken -> // Proceed with JWT processing (store it, use it for auth, etc.) println("Login successful! Token: $jwtToken, Code: ${loginResponse.code}") } ?: run { // Edge case: 200 but token is null (unlikely per your API spec) println("Login succeeded but no token received") } } ?: println("Empty response body for successful login") } response.code() == 403 -> { // Handle 403 Forbidden response response.errorBody()?.let { errorBody -> val errorResponse = Gson().fromJson(errorBody.string(), LoginResponse::class.java) println("Login failed: ${errorResponse.code}") // Show error to user, log details, etc. } ?: println("Empty error body for 403 response") } else -> { // Handle other status codes (404, 500, etc.) println("Unexpected status code: ${response.code()}") } } }
4. Key Notes to Avoid Pitfalls
- Converter Factory Compatibility: Ensure your chosen converter (Gson, Moshi, etc.) supports nullable fields. Gson handles this out of the box, but if you're using Moshi, make sure you've enabled
kotlinx.serializationor marked thetokenfield as nullable in your adapter. - Error Body Parsing: Always call
string()on the error body only once—it consumes the stream, so storing it in a variable first is safe if you need to reuse it. - Coroutine Safety: Since we're using
suspendfunctions, make sure you're callingperformLoginfrom a coroutine scope (likeviewModelScopein Android or a customCoroutineScope).
This approach matches how your iOS implementation likely handles the API, since it lets you explicitly manage both success and error responses without relying on Retrofit's default exception behavior.
内容的提问来源于stack exchange,提问作者netshark1000

