JakeWharton/retrofit2-kotlinx-serialization-converter

Empty Response Body Problem

GeePawHill opened this issue · 3 comments

I'm just getting started with Retrofit, and am having trouble with an empty response body. I have tried using both Void and Unit, and get the same error. I can confirm that the server is returning OK with an empty response.

I feel sure this is something silly I'm doing, but I just can't see it.

Thanks!
GeePaw

interface UsersApi {
    @POST("signup")
    suspend fun signup(@Body request: AuthRequest): Call<Void>
}


val contentType = "application/json".toMediaType()
var retrofit = Retrofit.Builder()
    .baseUrl("http://localhost:8080")
    .addConverterFactory(Json.asConverterFactory(contentType))
    .build()

val service = retrofit.create(UsersApi::class.java)

class EndToEndTest {
    @Test
    fun something() {
        runBlocking {
            try {
                val signup = service.signup(AuthRequest("geepaw", "password"))
                val response = signup.execute()
                assertThat(response.isSuccessful).isTrue()
            } catch (io: IOException) {
                println("IO Exception")
            } catch (http: HttpException) {
                println("Http Exception")
            }
        }
    }
}

The result I'm getting at runtime is this:

kotlinx.serialization.json.internal.JsonDecodingException: Cannot read Json element because of unexpected end of the input at path: $
JSON input: 
	at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:46)
	at za.co.wethinkcode.EndToEndTest$something$1.invokeSuspend(EndToEndTest.kt:37)
Caused by: kotlinx.serialization.json.internal.JsonDecodingException: Cannot read Json element because of unexpected end of the input at path: $
JSON input: 
	at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
	at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
	at kotlinx.serialization.json.internal.AbstractJsonLexer.fail(AbstractJsonLexer.kt:598)
	at kotlinx.serialization.json.internal.AbstractJsonLexer.fail$default(AbstractJsonLexer.kt:596)
	at kotlinx.serialization.json.internal.JsonTreeReader.read(JsonTreeReader.kt:104)
	at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeJsonElement(StreamingJsonDecoder.kt:50)
	at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:65)
	at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:81)
	at kotlinx.serialization.json.Json.decodeFromString(Json.kt:107)
	at com.jakewharton.retrofit2.converter.kotlinx.serialization.Serializer$FromString.fromResponseBody(Serializer.kt:26)
	at com.jakewharton.retrofit2.converter.kotlinx.serialization.DeserializationStrategyConverter.convert(DeserializationStrategyConverter.kt:11)
	at com.jakewharton.retrofit2.converter.kotlinx.serialization.DeserializationStrategyConverter.convert(DeserializationStrategyConverter.kt:7)
	at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:243)
	at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:153)
	at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:504)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)


Cannot read Json element because of unexpected end of the input at path: $
JSON input: 
kotlinx.serialization.json.internal.JsonDecodingException: Cannot read Json element because of unexpected end of the input at path: $
JSON input: 
	at app//kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
	at app//kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
	at app//kotlinx.serialization.json.internal.AbstractJsonLexer.fail(AbstractJsonLexer.kt:598)
	at app//kotlinx.serialization.json.internal.AbstractJsonLexer.fail$default(AbstractJsonLexer.kt:596)
	at app//kotlinx.serialization.json.internal.JsonTreeReader.read(JsonTreeReader.kt:104)
	at app//kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeJsonElement(StreamingJsonDecoder.kt:50)
	at app//kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:65)
	at app//kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:81)
	at app//kotlinx.serialization.json.Json.decodeFromString(Json.kt:107)
	at app//com.jakewharton.retrofit2.converter.kotlinx.serialization.Serializer$FromString.fromResponseBody(Serializer.kt:26)
	at app//com.jakewharton.retrofit2.converter.kotlinx.serialization.DeserializationStrategyConverter.convert(DeserializationStrategyConverter.kt:11)
	at app//com.jakewharton.retrofit2.converter.kotlinx.serialization.DeserializationStrategyConverter.convert(DeserializationStrategyConverter.kt:7)
	at app//retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:243)
	at app//retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:153)
	at app//okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:504)
	at java.base@17.0.8/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base@17.0.8/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base@17.0.8/java.lang.Thread.run(Thread.java:833)

You're oh so very close!

Returning Call<T> is something that the Retrofit core supports for synchronous methods. An instance is returned and then you can choose to synchronously execute() it or enqueue() it for asynchronous work.

Next, suspend support, also in the core of Retrofit, is a shortcut around Call.enqueue. Because of this, you can directly return T from the function when you add the suspend keyword.

Finally, when suspend is used the declared return type becomes the type that is used to deserialize the body. So Retrofit asks this converter for a serializer for Call<T> instead of just T.

I'm on mobile, but we should detect this case and fail at validation rather than asking for Call<T>. The documentation for suspend is also woefully lacking. I'll file issue for those on Retrofit later.

Hope that helps!

Got it!

In the suspend form, if there's no success-value, just make it a Unit, and if there's a success-value, use that type. If there's a problem in either case, the HttpException has what I need to diagnose the issue and inform the user.

The new code below works like a charm.

interface UsersApi {
    @POST("signup")
    suspend fun signup(@Body request: AuthRequest)

    @POST("signin")
    suspend fun signin(@Body request: AuthRequest): AuthResponse
}

Thank you so much for the speedy turnaround! I can now have a beer and code this Friday night away. :)