You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.22 11:18:11