Skip to content

Instantly share code, notes, and snippets.

@osipxd
Created September 4, 2022 18:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save osipxd/ae979d4978af717686671e285b6e3534 to your computer and use it in GitHub Desktop.
Save osipxd/ae979d4978af717686671e285b6e3534 to your computer and use it in GitHub Desktop.
package io.example
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Interceptor
import okhttp3.Response
import retrofit2.HttpException
import retrofit2.Invocation
import java.io.IOException
import java.io.InputStream
import kotlin.math.abs
import retrofit2.Response as RetrofitResponse
class ServerErrorsInterceptor(
private val json: Json,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (!response.isSuccessful) throwErrorForResponse(response)
return response
}
private fun throwErrorForResponse(response: Response) {
// We can't call body.string() because it closes stream and makes it
// unavailable for other interceptors and for retrofit.
// Read response body to buffer instead.
val source = response.body?.source() ?: return
source.request(Long.MAX_VALUE) // Add whole response to buffer
val bodyStream = source.buffer.clone().inputStream()
val errorResponse = parseErrorResponse(bodyStream)
throw ServerException(
message = errorResponse.error,
userMessage = errorResponse.message,
errorCode = errorResponse.code,
httpException = response.toHttpException(),
)
}
private fun parseErrorResponse(stream: InputStream): ErrorResponse {
// Specify here your logic to parse error body. Simplest possible logic used as example
return json.decodeFromStream(stream)
}
@Suppress("NOTHING_TO_INLINE") // We don't want this call in stacktrace, so make it inline
private inline fun Response.toHttpException(): HttpException {
val retrofitResponse = RetrofitResponse.error<Unit>(checkNotNull(body), this)
return HttpException(retrofitResponse)
.prependStackTrace(getRetrofitStackTraceElement())
}
private fun Response.getRetrofitStackTraceElement(): StackTraceElement? {
val method = (request.tag(Invocation::class.java) ?: return null).method()
// Kotlin adds suffix after hyphen for internal methods and methods having default arguments. Drop it.
// Also, append response code to the method name.
val fakeMethodName = "${method.name.substringBefore('-')}-$code"
// We don't know real line number for method, so we calculate fake line number.
// This value should be a positive number and should always be the same for equal method calls.
val fakeLineNumber = abs(method.declaringClass.name.hashCode() xor fakeMethodName.hashCode())
return StackTraceElement(
/* declaringClass = */ method.declaringClass.name,
/* methodName = */ fakeMethodName,
/* fileName = */ "${method.declaringClass.simpleName}.kt",
/* lineNumber = */ fakeLineNumber,
)
}
}
@Serializable
data class ErrorResponse(
val code: String? = null,
val message: String? = null,
val error: String? = null,
)
class ServerException(
message: String?,
val userMessage: String?,
val errorCode: String?,
val httpException: HttpException,
) : IOException(message, httpException) {
val httpCode: Int
get() = httpException.code()
}
package io.example
/** Adds the given [elements] to the start of the given stack trace. */
fun <T : Throwable> T.prependStackTrace(vararg elements: StackTraceElement?): T = apply {
stackTrace = (elements.filterNotNull() + stackTrace).toTypedArray()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment