Skip to content

Commit 9a4d382

Browse files
authored
Merge pull request #14 from haroldadmin/development
Handle successful responses with empty body
2 parents 1142359 + 14876d6 commit 9a4d382

File tree

11 files changed

+157
-62
lines changed

11 files changed

+157
-62
lines changed

src/main/kotlin/com/haroldadmin/cnradapter/DeferredNetworkResponseAdapter.kt

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.haroldadmin.cnradapter
2+
23
import kotlinx.coroutines.CompletableDeferred
34
import kotlinx.coroutines.Deferred
45
import okhttp3.ResponseBody
56
import retrofit2.*
6-
import java.io.IOException
77
import java.lang.reflect.Type
88

99
/**
@@ -17,8 +17,8 @@ import java.lang.reflect.Type
1717
*/
1818

1919
internal class DeferredNetworkResponseAdapter<T : Any, U : Any>(
20-
private val successBodyType: Type,
21-
private val errorConverter: Converter<ResponseBody, U>
20+
private val successBodyType: Type,
21+
private val errorConverter: Converter<ResponseBody, U>
2222
) : CallAdapter<T, Deferred<NetworkResponse<T, U>>> {
2323

2424
/**
@@ -54,20 +54,7 @@ internal class DeferredNetworkResponseAdapter<T : Any, U : Any>(
5454
}
5555

5656
override fun onResponse(call: Call<T>, response: Response<T>) {
57-
val headers = response.headers()
58-
val responseCode = response.code()
59-
val body = response.body()
60-
61-
val networkResponse = if (body != null) {
62-
NetworkResponse.Success(body, headers)
63-
} else {
64-
try {
65-
val convertedErrorBody = errorConverter.convert(response.errorBody())
66-
NetworkResponse.ServerError(convertedErrorBody, responseCode, headers)
67-
} catch (ex: Exception) {
68-
NetworkResponse.UnknownError(ex)
69-
}
70-
}
57+
val networkResponse = ResponseHandler.handle(response, successBodyType, errorConverter)
7158
deferred.complete(networkResponse)
7259
}
7360
})

src/main/kotlin/com/haroldadmin/cnradapter/ErrorExtraction.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import java.io.IOException
88
internal const val UNKNOWN_ERROR_RESPONSE_CODE = 520
99

1010
internal fun <E : Any> HttpException.extractFromHttpException(
11-
errorConverter: Converter<ResponseBody, E>
11+
errorConverter: Converter<ResponseBody, E>
1212
): NetworkResponse.ServerError<E> {
1313
val error = response()?.errorBody()
1414
val responseCode = response()?.code() ?: UNKNOWN_ERROR_RESPONSE_CODE
@@ -28,7 +28,7 @@ internal fun <E : Any> HttpException.extractFromHttpException(
2828
}
2929

3030
internal fun <S : Any, E : Any> Throwable.extractNetworkResponse(
31-
errorConverter: Converter<ResponseBody, E>
31+
errorConverter: Converter<ResponseBody, E>
3232
): NetworkResponse<S, E> {
3333
return when (this) {
3434
is IOException -> NetworkResponse.NetworkError(this)

src/main/kotlin/com/haroldadmin/cnradapter/Extensions.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import kotlinx.coroutines.delay
1515
* @return The NetworkResponse value whether it be successful or failed after retrying
1616
*/
1717
suspend inline fun <T : Any, U : Any> executeWithRetry(
18-
times: Int = 10,
19-
initialDelay: Long = 100, // 0.1 second
20-
maxDelay: Long = 1000, // 1 second
21-
factor: Double = 2.0,
22-
block: suspend () -> NetworkResponse<T, U>
18+
times: Int = 10,
19+
initialDelay: Long = 100, // 0.1 second
20+
maxDelay: Long = 1000, // 1 second
21+
factor: Double = 2.0,
22+
block: suspend () -> NetworkResponse<T, U>
2323
): NetworkResponse<T, U> {
2424
var currentDelay = initialDelay
2525
repeat(times - 1) {

src/main/kotlin/com/haroldadmin/cnradapter/NetworkResponse.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ sealed class NetworkResponse<out T : Any, out U : Any> {
1212
/**
1313
* A request that resulted in a response with a non-2xx status code.
1414
*/
15-
data class ServerError<U : Any>(val body: U?, val code: Int, val headers: Headers? = null) : NetworkResponse<Nothing, U>()
15+
data class ServerError<U : Any>(
16+
val body: U?,
17+
val code: Int,
18+
val headers: Headers? = null
19+
) : NetworkResponse<Nothing, U>()
1620

1721
/**
1822
* A request that didn't result in a response.
@@ -24,5 +28,5 @@ sealed class NetworkResponse<out T : Any, out U : Any> {
2428
*
2529
* An example of such an error is JSON parsing exception thrown by a serialization library.
2630
*/
27-
data class UnknownError(val error: Throwable): NetworkResponse<Nothing, Nothing>()
31+
data class UnknownError(val error: Throwable) : NetworkResponse<Nothing, Nothing>()
2832
}

src/main/kotlin/com/haroldadmin/cnradapter/NetworkResponseAdapter.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import retrofit2.Converter
77
import java.lang.reflect.Type
88

99
class NetworkResponseAdapter<S : Any, E : Any>(
10-
private val successType: Type,
11-
private val errorBodyConverter: Converter<ResponseBody, E>
10+
private val successType: Type,
11+
private val errorBodyConverter: Converter<ResponseBody, E>
1212
) : CallAdapter<S, Call<NetworkResponse<S, E>>> {
1313

1414
override fun responseType(): Type = successType
1515

1616
override fun adapt(call: Call<S>): Call<NetworkResponse<S, E>> {
17-
return NetworkResponseCall(call, errorBodyConverter)
17+
return NetworkResponseCall(call, errorBodyConverter, successType)
1818
}
1919
}
2020

src/main/kotlin/com/haroldadmin/cnradapter/NetworkResponseCall.kt

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,23 @@ package com.haroldadmin.cnradapter
22

33
import okhttp3.Request
44
import okhttp3.ResponseBody
5-
import okio.Timeout
65
import retrofit2.Call
76
import retrofit2.Callback
87
import retrofit2.Converter
98
import retrofit2.Response
9+
import java.lang.reflect.Type
1010

1111
internal class NetworkResponseCall<S : Any, E : Any>(
12-
private val backingCall: Call<S>,
13-
private val errorConverter: Converter<ResponseBody, E>
12+
private val backingCall: Call<S>,
13+
private val errorConverter: Converter<ResponseBody, E>,
14+
private val successBodyType: Type
1415
) : Call<NetworkResponse<S, E>> {
1516

1617
override fun enqueue(callback: Callback<NetworkResponse<S, E>>) = synchronized(this) {
1718
backingCall.enqueue(object : Callback<S> {
1819
override fun onResponse(call: Call<S>, response: Response<S>) {
19-
val body = response.body()
20-
val headers = response.headers()
21-
val code = response.code()
22-
val errorBody = response.errorBody()
23-
24-
if (response.isSuccessful) {
25-
if (body != null) {
26-
callback.onResponse(this@NetworkResponseCall, Response.success(NetworkResponse.Success(body, headers)))
27-
} else {
28-
// Response is successful but the body is null, so there's probably a server error here
29-
callback.onResponse(this@NetworkResponseCall, Response.success(NetworkResponse.ServerError(null, code, headers)))
30-
}
31-
} else {
32-
val networkResponse = try {
33-
val convertedBody = errorConverter.convert(errorBody)
34-
NetworkResponse.ServerError(convertedBody, code, headers)
35-
} catch(ex: Exception) {
36-
NetworkResponse.UnknownError(ex)
37-
}
38-
callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse))
39-
}
20+
val networkResponse = ResponseHandler.handle(response, successBodyType, errorConverter)
21+
callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse))
4022
}
4123

4224
override fun onFailure(call: Call<S>, throwable: Throwable) {
@@ -50,7 +32,11 @@ internal class NetworkResponseCall<S : Any, E : Any>(
5032
backingCall.isExecuted
5133
}
5234

53-
override fun clone(): Call<NetworkResponse<S, E>> = NetworkResponseCall(backingCall.clone(), errorConverter)
35+
override fun clone(): Call<NetworkResponse<S, E>> = NetworkResponseCall(
36+
backingCall.clone(),
37+
errorConverter,
38+
successBodyType
39+
)
5440

5541
override fun isCanceled(): Boolean = synchronized(this) {
5642
backingCall.isCanceled
@@ -66,5 +52,5 @@ internal class NetworkResponseCall<S : Any, E : Any>(
6652

6753
override fun request(): Request = backingCall.request()
6854

69-
override fun timeout() = backingCall.timeout()
55+
override fun timeout() = backingCall.timeout()
7056
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.haroldadmin.cnradapter
2+
3+
import okhttp3.ResponseBody
4+
import retrofit2.Converter
5+
import retrofit2.Response
6+
import java.lang.reflect.Type
7+
8+
/**
9+
* A utility object to centralize the logic of converting a [Response] to a [NetworkResponse]
10+
*/
11+
internal object ResponseHandler {
12+
13+
/**
14+
* Converts the given [response] to a subclass of [NetworkResponse] based on different conditions.
15+
*
16+
* If the server response is successful with:
17+
* => a non-empty body -> NetworkResponse.Success<S, E>
18+
* => an empty body (and [successBodyType] is Unit) -> NetworkResponse.Success<Unit, E>
19+
* => an empty body (and [successBodyType] is not Unit) -> NetworkResponse.ServerError<E>
20+
*
21+
* @param response Retrofit's response object supplied to the call adapter
22+
* @param successBodyType A [Type] representing the success body
23+
* @param errorConverter A retrofit converter to convert the error body into the error response type (E)
24+
* @param S The success body type generic parameter
25+
* @param E The error body type generic parameter
26+
*/
27+
fun <S : Any, E : Any> handle(
28+
response: Response<S>,
29+
successBodyType: Type,
30+
errorConverter: Converter<ResponseBody, E>
31+
): NetworkResponse<S, E> {
32+
val body = response.body()
33+
val headers = response.headers()
34+
val code = response.code()
35+
val errorBody = response.errorBody()
36+
37+
return if (response.isSuccessful) {
38+
if (body != null) {
39+
NetworkResponse.Success(body, headers)
40+
} else {
41+
// Special case: If the response is successful and the body is null, return a successful response
42+
// if the service method declares the success body type as Unit. Otherwise, return a server error
43+
if (successBodyType == Unit::class.java) {
44+
@Suppress("UNCHECKED_CAST")
45+
NetworkResponse.Success(Unit, headers) as NetworkResponse<S, E>
46+
} else {
47+
NetworkResponse.ServerError(null, code, headers)
48+
}
49+
}
50+
} else {
51+
val networkResponse: NetworkResponse<S, E> = try {
52+
val convertedBody = errorConverter.convert(errorBody)
53+
NetworkResponse.ServerError(convertedBody, code, headers)
54+
} catch (ex: Exception) {
55+
NetworkResponse.UnknownError(ex)
56+
}
57+
networkResponse
58+
}
59+
}
60+
61+
}

src/test/kotlin/com/haroldadmin/cnradapter/MoshiApplicationTest.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.squareup.moshi.Moshi
66
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
77
import io.kotlintest.matchers.string.shouldContain
88
import io.kotlintest.matchers.types.shouldBeInstanceOf
9+
import io.kotlintest.shouldBe
910
import io.kotlintest.specs.AnnotationSpec
1011
import kotlinx.coroutines.Deferred
1112
import kotlinx.coroutines.runBlocking
@@ -44,6 +45,12 @@ internal interface LaunchesService {
4445
fun launchForFlightNumberAsyncInvalid(
4546
@Path("flightNumber") flightNumber: Long
4647
): Deferred<NetworkResponse<LaunchInvalid, GenericErrorResponseInvalid>>
48+
49+
@GET("/health")
50+
suspend fun healthCheck(): NetworkResponse<Unit, GenericErrorResponse>
51+
52+
@GET("/health")
53+
fun deferredHealthCheck(): Deferred<NetworkResponse<Unit, GenericErrorResponse>>
4754
}
4855

4956
internal class TestApplication(
@@ -72,6 +79,13 @@ internal class TestApplication(
7279
launchesService.launchForFlightNumberAsyncInvalid(flightNumber).await()
7380
}
7481

82+
fun healthCheck(): NetworkResponse<Unit, GenericErrorResponse> = runBlocking {
83+
launchesService.healthCheck()
84+
}
85+
86+
fun deferredHealthCheck(): NetworkResponse<Unit, GenericErrorResponse> = runBlocking {
87+
launchesService.deferredHealthCheck().await()
88+
}
7589
}
7690

7791
internal class MoshiApplicationTest: AnnotationSpec() {
@@ -203,4 +217,30 @@ internal class MoshiApplicationTest: AnnotationSpec() {
203217
response as NetworkResponse.UnknownError
204218
response.error.shouldBeInstanceOf<JsonDataException>()
205219
}
220+
221+
@Test
222+
fun `should parse empty body as Unit`() {
223+
val app = TestApplication(service)
224+
server.enqueue(MockResponse().apply {
225+
setResponseCode(204)
226+
})
227+
val response = app.healthCheck()
228+
229+
response.shouldBeInstanceOf<NetworkResponse.Success<Unit>>()
230+
response as NetworkResponse.Success
231+
response.body shouldBe Unit
232+
}
233+
234+
@Test
235+
fun `should parse empty body as Unit for deferred methods too`() {
236+
val app = TestApplication(service)
237+
server.enqueue(MockResponse().apply {
238+
setResponseCode(204)
239+
})
240+
val response = app.deferredHealthCheck()
241+
242+
response.shouldBeInstanceOf<NetworkResponse.Success<Unit>>()
243+
response as NetworkResponse.Success
244+
response.body shouldBe Unit
245+
}
206246
}

src/test/kotlin/com/haroldadmin/cnradapter/NetworkResponseCallTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal class NetworkResponseCallTest: AnnotationSpec() {
2121
@Before
2222
fun setup() {
2323
backingCall = CompletableCall<String>()
24-
networkResponseCall = NetworkResponseCall<String, String>(backingCall, errorConverter)
24+
networkResponseCall = NetworkResponseCall<String, String>(backingCall, errorConverter, String::class.java)
2525
}
2626

2727
@Test(expected = UnsupportedOperationException::class)

src/test/kotlin/com/haroldadmin/cnradapter/Service.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ interface Service {
99

1010
@GET("/suspend")
1111
suspend fun getTextSuspend(): NetworkResponse<String, String>
12+
13+
@GET("/suspend-empty-body")
14+
suspend fun getEmptyBodySuspend(): NetworkResponse<Unit, String>
1215
}

0 commit comments

Comments
 (0)