9

I have an api that returns error body with the correct error information when a bad request is sent. For eg I get status code 400 and the following body -

{
  "errorCode": 1011,
  "errorMessage": "Unable to get Child information"
}

Now when I am writing a ktor client in a multi-platform module for this, I catch this in a response validator like -

 HttpResponseValidator {
            validateResponse {
                val statusCode = it.status.value
                when (statusCode) {
                    in 300..399 -> print(it.content.toString())
                    in 400..499 -> {
                        print(it.content.toString())
                        throw ClientRequestException(it)
                    }
                    in 500..599 -> print(it.content.toString())
                }
            }
            handleResponseException {
                print(it.message)
            }
        }

My query here is I am not able to access the response error body in either validateResponse or handleResponseException. Is there a way i can catch and parse that to get the actual error sent by server?

Kapil G
  • 4,081
  • 2
  • 20
  • 32

4 Answers4

13

You can declare a data class Error to represent the error response you expect.

import kotlinx.serialization.Serializable

@Serializable
data class Error(
    val errorCode: Int,   //if by errorCode you mean the http status code is not really necessary to include here as you already know it from the validateResponse
    val errorMessage: String
)

you can have a suspend fun to parse the response and have it as an instance of the Error data class

 suspend fun getError(responseContent: ByteReadChannel): Error {
    responseContent.readUTF8Line()?.let {
        return Json(JsonConfiguration.Stable).parse(Error.serializer(), it)
    }
    throw IllegalArgumentException("not a parsable error")
}

then inside the handleResponseException

handleResponseException { cause -> 
            val error = when (cause) {
                is ClientRequestException -> exceptionHandler.getError(cause.response.content)
// other cases here 

                else -> // throw Exception() do whatever you need 
            }
//resume with the error 
        }

you can implement some logic based on the error you get to throw an exception and catch it somewhere else in your code for example

when (error.errorCode) {
        1-> throw MyCustomException(error.errorMessage)
        else -> throw Exception(error.errorMessage)
    }

I hope it helps

dimitris boutas
  • 343
  • 4
  • 5
  • This doesnt seem to be able to parse the Json and breaks with the error kotlinx.serialization.json.JsonDecodingException: Unexpected JSON token at offset 1: Expected '}'. JSON input: { – Kapil G Jul 07 '20 at 17:57
  • @KapilG make sure your data class matches the expected json from the server. Alter the json configuration accordingly to cover all the cases (strict, ignore unknown keys etc). – dimitris boutas Jul 09 '20 at 09:37
  • the data class matches the JSON. It's just when you read the ByteReadChannel it just picks up the first line and not the whole JSON and tries to parse it. – Kapil G Jul 09 '20 at 11:28
  • I am guessing the main problem is how to read the byteReadChannel correctly and there is not much documentation on it. – Kapil G Jul 09 '20 at 12:18
  • You can read `ByteReadChannel` using `readRemaining()` – Leonid Jul 09 '20 at 13:13
  • Hi @Leonid do you have any pointers to any documentation or sample code which points how `readRemaining()` works? Considering Json parsing would need a string as input and `readRemaining` returns packets. I was hoping this would be much simpler than trying to figure out streams of data in the form of string. – Kapil G Jul 09 '20 at 13:30
  • Ok I got this to work thanks to both of you. @dimitrismpoutas if you change your answer from readLine to `readRemaining` I can accept it so that other users get the benefit. Basically my getError method looks like - ``` private suspend fun getError(responseContent: ByteReadChannel): KCZError { try { return Json(JsonConfiguration.Stable).parse(KCZError.serializer(), responseContent.readRemaining().readText()) } catch (e: Exception) { throw IllegalArgumentException("not a parsable error") } } ``` – Kapil G Jul 10 '20 at 15:04
  • 3
    just to share, as of today, I managed to only use `cause.response.readText()` in order to get my api error text. – agonist_ Feb 04 '21 at 11:32
  • same for me, only possible way to get this working was: `val errorJson = throwable.response.readText(Charset.defaultCharset())` – pinkfloyd Mar 23 '21 at 09:19
1

Just in case this helps someone else searching in this space, in my ktor service design, the response.readText made sense in my case:

try {

  httpClient.post...   

} catch(cre: ClientRequestException){

  runBlocking {

    val content = cre.response?.readText(Charset.defaultCharset())

    val cfResponse = Gson().fromJson(content, CfResponse::class.java)

    ...

  }

}
34m0
  • 5,755
  • 1
  • 30
  • 22
1

After spending hours, I got the error body with the below steps.

1. Define your model class for error. In my case it was something like

@Serializable
data class MyException(
    val message : String,
    val code : String,
    val type : String,
    val status_code : Int
) : RuntimeException()

You can see I've also extended custom class to RuntimeException, because I want my class behave like Exception class

2. Call the API

try {
     val mClient = KtorClientFactory().build()

     val res = mClient.post<MemberResponse>("${baseURL}user/login/") {
                //.....
               }
            
     emit(DataState.Success(res))

} catch (ex: Exception) {
    if (ex is ClientRequestException) {
        
        val res = ex.response.readText(Charsets.UTF_8)
        
        try {
            val myException = Json { ignoreUnknownKeys = true }
                              .decodeFromString(MyException.serializer(), res)

            emit(DataState.Error(myException))

         } catch (ex: Exception) {
              ex.printStackTrace()
           }
    } else
         emit(DataState.Error(ex))
}

That's it. You've parsed the error body.

To understand it in short way, you simply need to focus in two steps.

1. val res = ex.response.readText(Charsets.UTF_8)

2. val myException = Json { ignoreUnknownKeys = true }.decodeFromString(MyException.serializer(), res)

Ankit Dubey
  • 1,000
  • 1
  • 8
  • 12
0

As others have pointed out you can declare the data class of your Error object, and since Ktor is already setup with the serialiser, we can get the response body from ResponseException.

Here's an extension function for ease of use:

suspend inline fun <reified E> ResponseException.errorBody(): E? =
    try {
        response.body()
    } catch (e: SerializationException) {
        null
    }
Rahul Sainani
  • 3,437
  • 1
  • 34
  • 48