Handle empty body response from service - kotlin

Im trying to call a service via POST that sends an e-mail to the user, the body always return empty, I have to deal with the response code.
e.g. 204 = success.
Im trying to deal this way, but im not succeeding
Service:
#POST("xxxxxx")
fun resendBankSlip(#Path("userId") userId: Int): Deferred<Response>
ViewModel:
scope.launch {
try {
_loading.value = true
val response = repository.sendEmail(userId)
if (!response.isSuccessful) {
_error.value = R.string.generic_error_message
}
} catch (e: Throwable) {
_error.value = R.string.generic_error_message
} finally {
_loading.value = false
}
}
The error happens on val response = repository.sendEmail(userId)
Exception:
java.lang.IllegalArgumentException: 'okhttp3.Response' is not a valid response body type. Did you mean ResponseBody?
for method EmailService.sendEmail
Any idea?

You probably confused okhttp3.Response with retrofit.Response. Try to use retrofit2.Response wrapper in API response like that:
#POST("xxxxxx")
fun resendBankSlip(#Path("userId") userId: Int): Deferred<retrofit2.Response<Unit>>
After that you can easily get response code via response.code().
Also note that I passed Unit as Response's type argument because you don't need the body. In other cases you should pass an actual type of response body.

Related

Ktor how to get http code from request without body

I make a request to the server, but there is no body in the response.
Accordingly, the return value type of response is Unit.
suspend fun foo(
url: String,
id: Long
) {
val requestUrl = "$url/Subscriptions?id=${id}"
val response = httpApiClient.delete<Unit>(requestUrl) {
headers {
append(HttpHeaders.Authorization, createRequestToken(token))
}
}
return response
}
How in this case to receive the code of the executed request?
HttpResponseValidator {
validateResponse { response ->
TODO()
}
}
using a similar construction and throwing an error, for example, is not an option, since one http client is used for several requests, and making a new http client for one request is strange. is there any other way out?
You can specify the HttpResponse type as a type argument instead of Unit to get an object that allows you to access the status property (HTTP status code), headers, to receive the body of a response, etc. Here is an example:
import io.ktor.client.HttpClient
import io.ktor.client.engine.apache.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
suspend fun main() {
val client = HttpClient(Apache)
val response = client.get<HttpResponse>("https://httpbin.org/get")
// the response body isn't received yet
println(response.status)
}

Crash in coroutine

My function is quite straightforward,
Main Thread: Initializes a variable ->
Background Thread: Fire network request, assign the result back to the previous variable ->
Main Thread: Display that variable
Code below:
suspend fun createCity(context: Context, newCity: MutableLiveData<NewIdea>, mapBody: Map<String, String>, token: String) {
lateinit var response: NewIdea
try {
withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
response = webservice.createIdea(tripId, map, "Bearer $token")
getTrip(context, token)
}
} catch (e: Exception) {
Log.e(TAG, e.message)
}
newCity.value = response
}
But sometimes (it only happened 2 times actually) crashlytics reports crash for this line newCity.value = response
Fatal Exception: kotlin.UninitializedPropertyAccessException: lateinit property response has not been initialized
I don't really understand how that can happen.
Is this the correct way to return value from coroutine function?
thanks
Well if try block fails, it might happen that the lateinit variable isn't set at all. You should put the ui update code inside the try block as well, and handle the Exception separately:
Sidenote: withContext is well-optimized to return values, so you can make use of it.
suspend fun createCity(context: Context, newCity: MutableLiveData<NewIdea>, mapBody: Map<String, String>, token: String) {
try {
val response: NewIdea = withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
// does createIdea() first store it in var, then does getTrip(), then returns the result of createIdea() stored previously
webservice.createIdea(tripId, map, "Bearer $token").also { getTrip(context, token) } // ^withContext
}
newCity.value = response
} catch (e: Exception) {
Log.e(TAG, e.message)
}
}
A quick tip (optional): You can wrap the UI updating code with a withContext that dispatches the work to Dispatchers.Main when not running in main thread, while if running in main do nothing:
withContext(Dispatchers.Main.immediate) {
val response: NewIdea = withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
// does createIdea() first store it in var, then does getTrip(), then returns the result of createIdea() stored previously
webservice.createIdea(tripId, map, "Bearer $token").also { getTrip(context, token) } // ^withContext
}
newCity.value = response
}

Retrofit's `enqueue()` doesn't re-assign value of object in time for the return statement?

I'm making a call to the API and the response body is assigned to an object inside Retrofit's enqueue(), the problem is that enqueue finishes too quickly for the value to be assigned before the return statement of the function body is called.
Previously, I was using MutableLiveData before and it took care of that because it's always observing the data and when it changes it assigns it with no problem but now I don't want to use any MutableLiveData or Observables because I'm trying to prepare the data before any UI is actually drawn on the screen.
fun getResponse(
weatherLocationCoordinates: WeatherLocation
): RequestResponse {
weatherApiService.getCurrentWeather(
weatherLocationCoordinates.latitude,
weatherLocationCoordinates.longitude
).enqueue(object : Callback<WeatherResponse> {
override fun onResponse(
call: Call<WeatherResponse>,
response: Response<WeatherResponse>
) {
if (response.isSuccessful) {
// This where I do the assigning
requestResponse = RequestResponse(response.body(), true)
}
}
override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
requestResponse = RequestResponse(null, false)
}
})
// When this is called, enqueue is still not finished
// therefore I get the wrong value, I get the previously set initialization value of the obj.
return requestResponse
}
Should I be using Callbacks or something else? I'm not sure on how to implement the callback.
Following up on the comments here's an approach with callbacks:
Let's suppose we change the method signature to:
fun getResponse(
weatherLocationCoordinates: WeatherLocation,
onSuccess: (WeatherResponse) -> Unit = {},
onError: (Throwable) -> Unit = {}
) {
weatherApiService.getCurrentWeather(
weatherLocationCoordinates.latitude,
weatherLocationCoordinates.longitude
).enqueue(object : Callback<WeatherResponse> {
override fun onResponse(
call: Call<WeatherResponse>,
response: Response<WeatherResponse>
) {
if (response.isSuccessful) {
onSuccess(response.body())
} else {
onError(CustomHttpExceptionWithErrorDescription(response))
}
}
override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
onError(t)
}
})
}
CustomHttpExceptionWithErrorDescription will have to be something you code that can simply parse the error gotten from the server. Anything that is not 2XX status code
This method accepts 2 extra parameters - one gets called upon success the other on error. The idea is to call it like:
getResponse(
weatherLocationCoordinates,
onSuccess = {
// do something with response
},
onError = {
// do something with the error
}
)
Because they have default parameters you actually don't need to specify both callbacks. Just the one you are interested in. Examples:
// react only to successes
getResponse(
weatherLocationCoordinates,
onSuccess = {
// do something with response
}
)
// react only to errors
getResponse(weatherLocationCoordinates) {
// do something with the error
}
// just call the network calls and don't care about success or error
getResponse(weatherLocationCoordinates)

kotlin: retrofit2 getting 404 url not found error

Getting Response{protocol=http/1.1, code=404, message=Not Found, url=https://test.test.com/service/one}
The url is correct as postman works fine.
I have tried looking into this error but most things come back with URL was in correct. and the error itself is vague.
code that starts it. the builder is a json string that is valid. I have tested it in postman.
CoroutineScope(Dispatchers.Default).launch {
val call = submitService.submitCarton(builder.toString())
Log.d("submit", "begining")
withContext(Dispatchers.Main) {
if (call.isSuccessful) {
Log.d("submit",call.body() as String)
} else {
Log.d("submit", "else....")
}
}
}
service factory:
fun makeSubmitService() : SubmitService{
val url = "https://test.test.com/service/"
return Retrofit.Builder().baseUrl(url)
.client(okHttpClient).addConverterFactory(GsonConverterFactory.create())
.build().create(SubmitService::class.java)
}
interface:
interface SubmitService {
#POST("one")
suspend fun submitCarton(#Body json: String): Response<myModel>
}
Expected results are a json response however I am not getting that far.
edit: I created a okhttpclient and did a request manual and I get a message 200 ok.
code for my test
val JSON = MediaType.parse("application/json; charset=utf-8")
val client = OkHttpClient()
val body = "some json"
val requestBody = RequestBody.create(JSON, body)
val request = Request.Builder()
.url("https://test.test.com/service/one")
.post(requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(request: Request, e: IOException) {
Log.e("test", e.toString())
}
#Throws(IOException::class)
override fun onResponse(response: Response) {
Log.d("test", response.toString())
}
})
Solved it myself.
Issue was dumb, retrofit2 was giving 404 even though the web service was returning a error message.
added
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.1'
private val interceptor = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
private val okHttpClient = OkHttpClient().newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.build()
found out retrofit was sending a very unformatted string
"{ \"all my json filled with \" }"
instead of
{ json }
fixed it by adding
.addConverterFactory(ScalarsConverterFactory.create())
to my service factory
for anyone wondering why I am basically creating the json as a string instead of using a JSON object is because the service I talk to really really wants it to be in a very specific order which JSON just don't care about it however it wants it to look like JSON as well...

How to pass data between various parts of the pipeline in Ktor (Kotlin)

Am building an API and using intercept(ApplicationCallPipeline.Call){} to run some logic before each route execution. I need to pass data from the intercept() method to the called route and
am setting data by using call.attributes.put() in the intercept() like this:
val userKey= AttributeKey<User>("userK")
call.attributes.put(userKey, userData)
And retrieve userData with call.attributes[userKey] .
What happens is that call.attributes[userKey] only works in the intercept() method where I have set the attribute. It doesn't work in the route where I need it.
It throws me
java.lang.IllegalStateException: No instance for key AttributeKey: userK
I wonder if am doing things in the right way
Here is the simplest code reproducing what you describe:
class KtorTest {
data class User(val name: String)
private val userKey = AttributeKey<User>("userK")
private val expected = "expected name"
private val module = fun Application.() {
install(Routing) {
intercept(ApplicationCallPipeline.Call) {
println("intercept")
call.attributes.put(userKey, User(expected))
}
get {
println("call")
val user = call.attributes[userKey]
call.respond(user.name)
}
}
}
#Test fun `pass data`() {
withTestApplication(module) {
handleRequest {}.response.content.shouldNotBeNull() shouldBeEqualTo expected
}
}
}
I intercept the call, put the user in the attributes, and finally respond with the user in the get request.
The test passes.
What ktor version are you using and which engine?