Kapil G
Kapil G

Reputation: 4141

Parsing an error body in a ktor HTTPClient

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?

Upvotes: 10

Views: 13737

Answers (4)

Rahul Sainani
Rahul Sainani

Reputation: 3717

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
    }

Upvotes: 0

Ankit Dubey
Ankit Dubey

Reputation: 1170

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)

Upvotes: 1

34m0
34m0

Reputation: 5955

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)

    ...

  }

}

Upvotes: 1

dimitris boutas
dimitris boutas

Reputation: 373

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

Upvotes: 16

Related Questions