question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Kotlin Coroutines + OkHttp + Retrofit throw errors on 204 responses

See original GitHub issue

What kind of issue is this?

  • Question. This issue tracker is not the place for questions. If you want to ask how to do something, or to understand why something isn’t working the way you expect it to, use Stack Overflow. https://stackoverflow.com/questions/tagged/retrofit

  • Bug report. If you’ve found a bug, spend the time to write a failing test. Bugs with tests get fixed. Here’s an example: https://gist.github.com/swankjesse/6608b4713ad80988cdc9

  • Feature Request. Start by telling us what problem you’re trying to solve. Often a solution already exists! Don’t send pull requests to implement new features without first getting our support. Sometimes we leave features out on purpose to keep the project small.

Retrofit version: Tested on 2.7.0 and 2.9.0 and getting the same issue.

Expected: No error.

Actual:

Exception in thread "main" kotlin.KotlinNullPointerException: Response from com.example.retrofit_okhttp_kotlin.WebInterface.logout was null but response body type was declared as non-null
	at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:43)
	at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
	at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

Reproduce code:

package com.example.retrofit_okhttp_kotlin

import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import retrofit2.Retrofit
import retrofit2.http.GET

fun main() = runBlocking {
    val server = MockWebServer()
    server.enqueue(MockResponse().setResponseCode(204))
    server.start()

    val okhttp = OkHttpClient.Builder()
        .build()
    val retrofit = Retrofit.Builder()
        .baseUrl(server.url("/user/revoke/"))
        .client(okhttp)
        .build()
        .create(WebInterface::class.java)

    retrofit.logout()
}

interface WebInterface {
    @GET("user/revoke")
    suspend fun logout()
}


Other notes:

Tried all solutions in https://github.com/square/retrofit/issues/1554 but no avail because in retrofit2.OkHttpCall#parseResponse it is hardcoded to return null body upon receiving 204 or 205 response codes and skipped any response converters that I would have added.

 Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
    ResponseBody rawBody = rawResponse.body();

    // Remove the body's source (the only stateful object) so we can pass the response along.
    rawResponse = rawResponse.newBuilder()
        .body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
        .build();

    int code = rawResponse.code();
    if (code < 200 || code >= 300) {
      try {
        // Buffer the entire body to avoid future I/O.
        ResponseBody bufferedBody = Utils.buffer(rawBody);
        return Response.error(bufferedBody, rawResponse);
      } finally {
        rawBody.close();
      }
    }

    if (code == 204 || code == 205) { // <-- entered this branch
      rawBody.close();
      return Response.success(null, rawResponse); // <-- returns here
    }

    ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody);
    try {
      T body = responseConverter.convert(catchingBody); // <-- All converters I would ever add would be here, which is never run in this case
      return Response.success(body, rawResponse);
    } catch (RuntimeException e) {
      // If the underlying source threw an exception, propagate that rather than indicating it was
      // a runtime exception.
      catchingBody.throwIfCaught();
      throw e;
    }
  }

This response is promptly sent to retrofit2.KotlinExtensions which hardcoded an error case with a null body.

override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) { // <-- entered this branch
            val invocation = call.request().tag(Invocation::class.java)!!
            val method = invocation.method()
            val e = KotlinNullPointerException("Response from " +
                method.declaringClass.name +
                '.' +
                method.name +
                " was null but response body type was declared as non-null")
            continuation.resumeWithException(e) // <-- error thrown here
          } else {
            continuation.resume(body)
          }
        } else {
          continuation.resumeWithException(HttpException(response))
        }
      }

Also checked https://github.com/square/retrofit/issues/2494 but the reporter was not using Kotlin Update: May be related to https://github.com/square/retrofit/issues/3075

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:2
  • Comments:7

github_iconTop GitHub Comments

1reaction
victor-munozcommented, Mar 6, 2022

Thanks lukaszkalnik, Sadly, I need to perform specific actions in case of 4xx or 5xx responses, so do not throw an exception will be a problem.

Currently I am replacing the 204 code with a 200 code using a NetworkInterceptor to avoid trowing a KotlinNullPointerException

          OkHttpClient.Builder().addNetworkInterceptor { chain ->
                val request = chain.request()
                val response: Response = chain.proceed(request)
                if(response.code() == 204) response.newBuilder().code(200).build()
                else response
            }.build().run {
                Retrofit.Builder()
                    .baseUrl(YOUR_URL)
                    .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
                    .addCallAdapterFactory(CoroutineCallAdapterFactory())
                    .client(this)
                    .build()
                    .create(YourService::class.java)
            }
0reactions
mtwolakcommented, Sep 7, 2022

We had a production incident because of this bug. Sadly we also had to make some manual workarounds.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to handle 204 response in Retrofit using Kotlin Coroutines?
You can use Response<Unit> to handle 204 responses using Retrofit but when you handle like this Retrofit will not throw an exception for...
Read more >
Retrofit2 doesn't handle empty content responses - Ken Yee
The workaround is to add a null delegating converter in front of the JSON converter for Retrofit2. class NullOnEmptyConverterFactory implements Converter.
Read more >
Retrofit — Effective error handling with Kotlin Coroutine and ...
When the OkHttp client receives a response from the server, it passes the response back to the Retrofit. Retrofit then pushes the meaningless...
Read more >
How to handle 204 response in Retrofit using Kotlin Coroutines
Java – OkHttpClient broken after updated Retrofit to Retrofit 2. Had the same problem as you. First of all, throw out: compile 'com.squareup.okhttp3:okhttp...
Read more >
How to handle RESTful web Services using Retrofit, OkHttp ...
Developers are required not to block it to avoid the app freezing or even crashing with an Application Not Responding dialog. Kotlin coroutines...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found