使用Kotlin+Retrofit2调用PUT登录API时返回415错误的排查求助
Kotlin+Retrofit2调用PUT登录API时返回415错误的排查求助
大家好,我现在正在把Android项目从Java+Volley迁移到Kotlin+Retrofit,遇到了一个棘手的问题:调用已经在生产环境稳定运行6年的PUT登录API时,一直返回415错误。这个API在官网、iOS应用和旧版Android应用里都正常工作,而且我用Postman测试也完全没问题,所以肯定是我Retrofit的配置哪里出问题了。
问题背景
这个登录API的要求很明确:
- 请求方法为PUT
- 需要自定义Content-Type:
application/vnd.slsdist.api+json;version=3 - 请求体必须包含
userId、password、resetAPIKey三个字段
我尝试用Interceptor设置Content-Type,日志里也能看到这个Header被成功带上,但还是返回415。另外,同一个服务器的GET状态接口(不需要特殊Header和请求体)可以正常调用,说明基础的Retrofit配置是没问题的。
查了下415错误的原因,通常和Content-Type不匹配或者请求体解析失败有关,但我不确定是Interceptor设置Header的方式不对,还是请求体的User对象序列化有问题,或者是其他我没考虑到的细节。
下面是我的相关代码,麻烦大家帮忙看看哪里出错了:
依赖配置(build.gradle)
plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { namespace 'com.example.gmailclone' compileSdk 34 defaultConfig { applicationId "com.example.gmailclone" minSdk 29 targetSdk 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary true } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion '1.4.7' } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } } dependencies { implementation 'com.squareup.moshi:moshi:1.15.0' implementation 'com.squareup.moshi:moshi-kotlin:1.15.0' // was 1.9.3 implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' // was 2.3.1 implementation 'androidx.activity:activity-compose:1.7.2' // was 1.5.1 implementation platform('androidx.compose:compose-bom:2022.10.00') implementation 'androidx.navigation:navigation-compose:2.6.0' implementation "androidx.navigation:navigation-fragment-ktx:2.6.0" implementation "androidx.navigation:navigation-ui-ktx:2.6.0" implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material:material' implementation 'androidx.core:core-ktx:1.10.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' }
请求体数据类
data class User( val userId: String, val password: String, val resetAPIKey: String )
Retrofit接口定义
import com.example.gmailclone.models.RegisterResponse import com.example.gmailclone.models.StatusResponse import com.example.gmailclone.models.User import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Headers import retrofit2.http.PUT interface GoCartService { @Headers("Accept: */*") @GET("status") fun getStatus(): Call<StatusResponse> @PUT("register") fun login(@Body user: User) : Call<RegisterResponse> }
Content-Type拦截器
import okhttp3.Interceptor import okhttp3.Response object ContentTypeInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() .newBuilder() .header("Content-Type", "application/vnd.slsdist.api+json;version=3") .build() return chain.proceed(request) } }
Retrofit配置类
import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory object Api { private val BASE_URL = "https://ecomrl.slsdist.com/esls/rest/mobileservice/" private val okHttpClient = OkHttpClient() .newBuilder() .addInterceptor(RequestInterceptor) .addInterceptor(ContentTypeInterceptor) .build() private val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() // Status API, does NOT need special Content-Type, so build retrofit object without client // and without converter factory private val statusRetrofit = Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .baseUrl(BASE_URL) .build() val statusRetrofitService: GoCartService by lazy { statusRetrofit.create(GoCartService::class.java) } // Create LoginAPI (Register) retrofit object using Retrofit Builder private val retrofit = Retrofit.Builder() .client(okHttpClient) .addConverterFactory(MoshiConverterFactory.create(moshi)) .baseUrl(BASE_URL) .build() val retrofitService: GoCartService by lazy { retrofit.create(GoCartService::class.java) } }
网络请求管理类
import android.content.ContentValues.TAG import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import com.example.gmailclone.models.ErrorResponse import com.example.gmailclone.models.RegisterResponse import com.example.gmailclone.models.StatusResponse import com.example.gmailclone.models.User import retrofit2.Call import retrofit2.Callback import retrofit2.Response class NetworkingManager { var responseTag: String = "API->" private val _statusResponse = mutableStateOf(StatusResponse()) private val _registerResponse = mutableStateOf(RegisterResponse()) private val _errorResponse = mutableStateOf(ErrorResponse()) val statusResponse: State<StatusResponse> @Composable get() = remember { _statusResponse } val registerResponse: State<RegisterResponse> @Composable get() = remember { _registerResponse } val errorResponse: State<ErrorResponse> @Composable get() = remember { _errorResponse } init { getStatus() } private fun getStatus() { responseTag = "${responseTag}Status" val service = Api.statusRetrofitService.getStatus() service.enqueue(object: Callback<StatusResponse> { override fun onResponse( call: Call<StatusResponse>, response: Response<StatusResponse> ) { if(response.isSuccessful) { _statusResponse.value = response.body()!! Log.d(responseTag, "${_statusResponse.value}") } else { Log.d(responseTag, "${response.errorBody()}") } } override fun onFailure(call: Call<StatusResponse>, t: Throwable) { Log.d(responseTag, "${t.printStackTrace()}") } }) } fun login(user: User) { Log.d(TAG, "login: In NewtorkingManager.login() user.userId is: ${user}") responseTag = "${responseTag}Login" val service = Api.retrofitService.login(user) service.enqueue(object: Callback<RegisterResponse> { override fun onResponse( call: Call<RegisterResponse>, response: Response<RegisterResponse> ) { if(response.isSuccessful) { _registerResponse.value = response.body()!! Log.d(responseTag, "Successful Login!! ${_registerResponse.value}") } else { val errorString = response.errorBody()?.byteStream()?.bufferedReader().use { it?.readText() } Log.d(responseTag, "Response body NOT successful!! ${errorString}") Log.d(responseTag, "HttpStatusCode: ${response.code()}") Log.d(responseTag, "HttpStatusMessage: ${response.message()}") Log.d(responseTag, "ResponseHeaders: ${response.headers()}") } } override fun onFailure(call: Call<RegisterResponse>, t: Throwable) { Log.d(responseTag, "${t.printStackTrace()}") } }) } }
登录界面(调用登录的入口)
package com.example.gmailclone.ui.screen import android.content.ContentValues.TAG import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Person import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.example.gmailclone.components.DevicesPreview import com.example.gmailclone.models.User import com.example.gmailclone.networking.NetworkingManager import com.example.gmailclone.ui.theme.GmailcloneTheme @Composable fun LogInScreen(navController: NavController, networkingManager: NetworkingManager) { val status = networkingManager.statusResponse.value.status val mobileLogoURI = networkingManager.statusResponse.value.mobileLogoURI val ecomVer = networkingManager.statusResponse.value.ecomVersion Log.d(TAG, "GmailApp: status is: $status") Log.d(TAG, "GmailApp: mobileLogoURI is: $mobileLogoURI") Log.d(TAG, "GmailApp: ecomVer is: $ecomVer") var userId by remember { mutableStateOf("sls070") } var password by remember { mutableStateOf("xxxxxxxxxxxx") } Card { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .padding(top = 100.dp) .fillMaxSize(), verticalArrangement = Arrangement.Top ) { Text(text = "LogIn Screen", fontWeight = FontWeight.SemiBold) OutlinedTextField( value = userId, onValueChange = { userId = it }, label = { Text("User Id") }, placeholder = { Text("Enter User Id") }, leadingIcon = { Icon(Icons.Filled.Person, "", modifier = Modifier.padding(16.dp)) } ) Spacer(modifier = Modifier.padding(vertical = 16.dp)) OutlinedTextField( value = password, onValueChange = { password = it }, label = { Text("Password") }, placeholder = { Text("Enter Password") }, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), leadingIcon = { Icon(Icons.Filled.Lock, "", modifier = Modifier.padding(16.dp)) } ) Spacer(modifier = Modifier.padding(vertical = 16.dp)) Button(onClick = { Log.d(TAG, "GmailApp: Logged in.") // Create User object and log in Log.d(TAG, "LogInScreen: User Id is: $userId") Log.d(TAG, "LogInScreen: Password is: $password") val user = User(userId = userId, password = password, resetAPIKey = "true") networkingManager.login(user) navController.navigate("LoginSuccess") }) { Text(text = "Login") } } } } @DevicesPreview @Composable fun PreviewLogInScreen() { GmailcloneTheme { Surface(color = MaterialTheme.colors.background) { val navController = rememberNavController() LogInScreen(navController = navController, networkingManager = NetworkingManager()) } } }
已经折腾好几天了,实在找不到问题所在,希望大家能帮我看看哪里出错了,万分感谢!
备注:内容来源于stack exchange,提问作者jonathan3087
相关产品推荐
相关产品推荐

