Skip to content

Commit 10cbc3e

Browse files
authored
Merge pull request #26 from haroldadmin/development
Add ability to parse status code and headers to NetworkResponse.UnknownError
2 parents 76c3ae4 + f56df54 commit 10cbc3e

File tree

5 files changed

+90
-45
lines changed

5 files changed

+90
-45
lines changed

docs/docs/docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ data class DetailsError(
3030
)
3131

3232
interface Api {
33-
@Get("/details)
33+
@Get("/details")
3434
suspend fun details(): NetworkResponse<DetailsResponse, DetailsError>
3535
}
3636

docs/docs/docs/special-cases.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,19 @@ val retrofit = Retrofit.Builder()
3131
.baseUrl("...")
3232
.build()
3333
```
34+
35+
## Status code and Headers in `NetworkResponse.UnknownError`
36+
37+
Network requests that result in a `NetworkResponse.UnknownError` can still convey useful information through their headers and status code. Unfortunately, it is not always possible to extract these values from a failed request.
38+
39+
Therefore, the `NetworkResponse.UnknownError` class contains nullable fields for the status code and headers. These fields are populated if their values can be extract from a failed request.
40+
41+
```kotlin
42+
data class UnknownError(
43+
val error: Throwable,
44+
val code: Int? = null,
45+
val headers: Headers? = null,
46+
) : NetworkResponse<Nothing, Nothing>()
47+
```
48+
49+
It is possible to extract this information from a failed request in most cases. However, if the server responds with a successful status code (200 <= code < 300) and an invalid body (which can not be parsed correctly), Retrofit assumes the network request failed. It forwards only the raised error and the original call to the registered call adapter, and thus all information about the response is lost resulting in a `NetworkResponse.UnknownError` with null `code` and `headers`.

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,9 @@ sealed class NetworkResponse<out T : Any, out U : Any> {
3232
*
3333
* An example of such an error is JSON parsing exception thrown by a serialization library.
3434
*/
35-
data class UnknownError(val error: Throwable) : NetworkResponse<Nothing, Nothing>()
35+
data class UnknownError(
36+
val error: Throwable,
37+
val code: Int? = null,
38+
val headers: Headers? = null,
39+
) : NetworkResponse<Nothing, Nothing>()
3640
}

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

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,52 +10,56 @@ import java.lang.reflect.Type
1010
*/
1111
internal object ResponseHandler {
1212

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()
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()
3636

37-
return if (response.isSuccessful) {
38-
if (body != null) {
39-
NetworkResponse.Success(body, headers, code)
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, code) as NetworkResponse<S, E>
46-
} else {
47-
NetworkResponse.ServerError(null, code, headers)
48-
}
49-
}
37+
return if (response.isSuccessful) {
38+
if (body != null) {
39+
NetworkResponse.Success(body, headers, code)
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, code) as NetworkResponse<S, E>
5046
} 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
47+
NetworkResponse.ServerError(null, code, headers)
5848
}
49+
}
50+
} else {
51+
val networkResponse: NetworkResponse<S, E> = try {
52+
val convertedBody = if (errorBody == null) {
53+
null
54+
} else {
55+
errorConverter.convert(errorBody)
56+
}
57+
NetworkResponse.ServerError(convertedBody, code, headers)
58+
} catch (ex: Exception) {
59+
NetworkResponse.UnknownError(ex, code = code, headers = headers)
60+
}
61+
networkResponse
5962
}
63+
}
6064

6165
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal data class GenericErrorResponse(val error: String)
2525

2626
internal data class GenericErrorResponseInvalid(@Json(name = "errrorrrr") val error: String)
2727

28+
@Suppress("DeferredIsResult")
2829
internal interface LaunchesService {
2930
@GET("launches/{flightNumber}")
3031
suspend fun launchForFlightNumber(
@@ -143,12 +144,32 @@ internal class MoshiApplicationTest: AnnotationSpec() {
143144
server.enqueue(MockResponse().apply {
144145
setBody(resourceFileContents("/falconsat_launch.json"))
145146
setResponseCode(200)
147+
setHeader("test", "true")
146148
})
147149
val response = app.getLaunchWithFailure(validFlightNumber)
148150

149151
response.shouldBeInstanceOf<NetworkResponse.UnknownError>()
150152
response as NetworkResponse.UnknownError
151153
response.error.shouldBeInstanceOf<JsonDataException>()
154+
response.code shouldBe null
155+
response.headers shouldBe null
156+
}
157+
158+
@Test
159+
fun shouldParseResponseCodeAndHeadersOfUnsuccessfulRequestWithInvalidBodyCorrectly() {
160+
val app = TestApplication(service)
161+
server.enqueue(MockResponse().apply {
162+
setBody("""{ "message": "Too many requests!" }""")
163+
setResponseCode(429)
164+
setHeader("test", "true")
165+
})
166+
val response = app.getLaunchWithFailure(validFlightNumber)
167+
168+
response.shouldBeInstanceOf<NetworkResponse.UnknownError>()
169+
response as NetworkResponse.UnknownError
170+
response.error.shouldBeInstanceOf<JsonDataException>()
171+
response.code shouldBe 429
172+
response.headers!!["test"] shouldBe "true"
152173
}
153174

154175
@Test

0 commit comments

Comments
 (0)