jonathan3087
jonathan3087

Reputation: 327

PUT with Retrofit2 using Kotlin fails with 415 Server using Reqeust @Body Object in Android

I want to call a login api that I've been using for over 6 years, so I know the API is good. It is already in production, and used for our website, iOS app, and Android app. My current project is just to upgrade my Android app from using Java & Volley to Kotlin & Retrofit. So, I know 100% the API works, and Android can do it. For now I have built a prototype of just a login screen using Kotlin and Retrofit, just to prove I can get Retrofit working with our login API, and that is where I am stuck.

It is a PUT API call and I need to pass in a custom 'Content-Type' of 'application/vnd.slsdist.api+json;version=3', and in the Request Body I need to pass in: "userId", "password", and "resetAPIKey".

I have read every piece of documentation and example I can find on Retrofit but still have not found a solution.

I am currently getting a 415 server error when I call the API from my code, which is usually a Content-Type error, I have tried many ways to set this and am currently using an Interceptor, which is my preferred way. I am logging this and can see that I am setting these and sending the Content-Type, and I'm sending the User object in the @Body property. A deeper dive into 415 Server codes says that actually it could be that Content-Type wasn't sent, was rejected, or the actual data couldn't be read. So, I'm not sure if the problem is with the way I am setting the Content-Type in the Interceptor or with the way I am setting the User object and using that in the @Body property of the API request. (And I guess it could be something completely different.). I will say, that I am able to call a simpler GET API to my same server that just connects and asks for a status, it returns a simple "running". So I know I can go a simple GET that doesn't need a Content-Type nor a Body, so there is that. Ok, enough explanation, here's the code:

Here is my successful call to Postman: NOTE: resetAPIKey is a String enter image description here

Here's my buiild.gradle so you can see the versions of retrofit, moshi, and converters I have installed:

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'
}

Here's my User object that I'll use to pass in those fields into the Request Body:

data class User(
    val userId: String,
    val password: String,
    val resetAPIKey: String
)

Here is the file that sets up the two services that I mentioned for the server status (that is working) and for register (login, currently giving the 415 error):

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>
}

Here's the ContentTypeInterceptor:

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)
    }
}

Here's the Api file where I build the OkHttpClient, the Intereptor, the MoshiBuilder, Retrofit, and put it all together:

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)
    }

}

Here's NetworkingManager:

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()}")
            }

        })
    }
}

Lastly, here's the LogInScreen, where at the bottom, you'll find a Jetpack Compose Button that in its onClick networkingManager.login(user) gets called. Thanks in advance for any and all help anyone can provide. I sure have looked everywhere. :)

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())
        }
    }
}

Upvotes: 0

Views: 369

Answers (1)

jonathan3087
jonathan3087

Reputation: 327

I'm not sure where this is documented, but I just found a comment on another post that said that Retrofit2 has dropped support for using Interceptors.

I changed my code to add the header, inline in the GoCartService.kt Interface like below, and now it works.

@Headers("Content-Type: application/vnd.slsdist.api+json;version=3")
    @PUT("register")
    fun login(@Body user: User): Call<RegisterResponse>

Upvotes: 0

Related Questions