Introduction

Retrofit's Response<T> wrapper returns body() as null in several scenarios beyond just HTTP errors: empty response bodies, mismatched content types, or 204 No Content responses. When code assumes a non-null body on successful requests (response.isSuccessful), it crashes with NullPointerException or fails to handle the edge case properly.

Symptoms

  • response.body() is null even though response.isSuccessful is true
  • NullPointerException when accessing response.body()!!
  • 204 No Content returns body() = null as expected but code does not handle it
  • Server returns Content-Type: text/plain but Retrofit expects JSON
  • DELETE and PATCH endpoints return null bodies

Example problem: ```kotlin @DELETE("users/{id}") suspend fun deleteUser(@Path("id") id: String): Response<User>

// In calling code: val response = api.deleteUser("123") if (response.isSuccessful) { val user = response.body()!! // CRASH: body is null for 204 response updateUI(user) } ```

Common Causes

  • HTTP 204 No Content has no body by definition
  • Server returns 200 OK with empty body {} or ""
  • Content-Type header does not match the converter factory
  • Response type declared as non-nullable when API can return null
  • Error responses return error body but body() is still null

Step-by-Step Fix

  1. 1.Handle nullable response body properly:
  2. 2.```kotlin
  3. 3.suspend fun deleteUser(userId: String): Result<Unit> {
  4. 4.return try {
  5. 5.val response = api.deleteUser(userId)
  6. 6.if (response.isSuccessful) {
  7. 7.Result.success(Unit) // No body expected
  8. 8.} else {
  9. 9.Result.failure(HttpException(response))
  10. 10.}
  11. 11.} catch (e: Exception) {
  12. 12.Result.failure(e)
  13. 13.}
  14. 14.}
  15. 15.`
  16. 16.Use Unit for endpoints that do not return a body:
  17. 17.```kotlin
  18. 18.// Correct API definition for no-body responses
  19. 19.@DELETE("users/{id}")
  20. 20.suspend fun deleteUser(@Path("id") id: String): Response<Unit>

@POST("users/{id}/activate") suspend fun activateUser(@Path("id") id: String): Response<Void>

// Calling code: val response = api.deleteUser("123") if (response.isSuccessful) { // No need to access body showSuccessMessage() } else { showError(response.message()) } ```

  1. 1.Parse error body when response fails:
  2. 2.```kotlin
  3. 3.suspend fun safeApiCall(): Result<UserData> {
  4. 4.return try {
  5. 5.val response = api.getUser()
  6. 6.if (response.isSuccessful) {
  7. 7.Result.success(response.body()!!)
  8. 8.} else {
  9. 9.// Parse error body
  10. 10.val errorBody = response.errorBody()?.string()
  11. 11.val errorResponse = try {
  12. 12.Gson().fromJson(errorBody, ErrorResponse::class.java)
  13. 13.} catch (e: Exception) {
  14. 14.ErrorResponse(message = "Unknown error")
  15. 15.}
  16. 16.Result.failure(ApiException(errorResponse.message, response.code()))
  17. 17.}
  18. 18.} catch (e: IOException) {
  19. 19.Result.failure(NetworkException(e))
  20. 20.}
  21. 21.}
  22. 22.`
  23. 23.Use a sealed class for API results:
  24. 24.```kotlin
  25. 25.sealed class ApiResult<out T> {
  26. 26.data class Success<T>(val data: T) : ApiResult<T>()
  27. 27.data class Error(val code: Int, val message: String) : ApiResult<Nothing>()
  28. 28.data class NetworkError(val exception: IOException) : ApiResult<Nothing>()
  29. 29.}

suspend fun <T : Any> safeCall(call: suspend () -> Response<T>): ApiResult<T> { return try { val response = call() if (response.isSuccessful) { val body = response.body() if (body != null) { ApiResult.Success(body) } else { ApiResult.Success(Unit as T) } } else { ApiResult.Error(response.code(), response.message()) } } catch (e: IOException) { ApiResult.NetworkError(e) } } ```

Prevention

  • Declare return type as Response<Unit> for endpoints without response bodies
  • Never use response.body()!! without checking response.isSuccessful
  • Use a result wrapper that handles success, error, and network failure cases
  • Add interceptors that log request/response bodies for debugging
  • Write integration tests against the actual API, not just mocks
  • Use Moshi or Gson with null-safe types in your data classes