ktorio / ktor

Framework for quickly creating connected applications in Kotlin with minimal effort
https://ktor.io
Apache License 2.0
12.97k stars 1.06k forks source link

Override exceptions from the http client with a feature? #884

Closed ScottPierce closed 5 years ago

ScottPierce commented 5 years ago

I haven't been able to find a way to override the BadResponseStatusException with a ktor feature. My team has previously overridden exceptions to a common format so that they are easier to handle. We also don't like to leak the HttpClient code into the entire application, but instead like to create a service module, where it's completely encapsulated from the rest of the application.

We've tried a number of ways to override the exception using features, but nothing has quite worked as we've expected. If this is currently possible, can you please indicate how this can be done, otherwise please add this as a feature request.

zhuosun-rally commented 5 years ago

mark

e5l commented 5 years ago

Hi, @ScottPierce. Thanks for the report As workaround you could disable expect success feature:

HttpClient {
    expectSuccess = false
}
ScottPierce commented 5 years ago

@e5l Doesn't work. If we look at https://github.com/ktorio/ktor/blob/master/ktor-client/ktor-client-core/common/src/io/ktor/client/call/HttpClientCall.kt#L54-L62 the problem is that all exceptions that aren't BadResponseStatusException are overridden.

I added:

HttpClient(ServicePlatform.httpClientEngineFactory) {
      expectSuccess = false
}

And also:

scope.responsePipeline.intercept(HttpResponsePipeline.Receive) {
  val response = subject.response as HttpResponse
  if (!response.status.isSuccess()) throw RuntimeException("Http Status: ${response.status.value}")
  proceedWith(subject)
}

My result is:

io.ktor.client.call.ReceivePipelineException: Fail to run receive pipeline
    at io.ktor.client.call.HttpClientCall.receive(HttpClientCall.kt:61)
    at io.ktor.client.call.HttpClientCall$receive$1.invokeSuspend(Unknown Source:12)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
    at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(PipelineContext.kt:183)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:160)
    at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(PipelineContext.kt:63)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:111)
    at io.ktor.util.pipeline.SuspendFunctionGun.execute(PipelineContext.kt:131)
    at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:24)
    at io.ktor.client.call.HttpClientCall.receive(HttpClientCall.kt:55)
    at com.example.api.service.AuthServiceImpl.getNonce(AuthService.kt:120)
    at com.example.api.service.AuthServiceImpl$getNonce$1.invokeSuspend(Unknown Source:32)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
    at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(PipelineContext.kt:183)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:142)
    at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(PipelineContext.kt:63)
    at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(PipelineContext.kt:92)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:45)
    at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(PipelineContext.kt:183)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:142)
    at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(PipelineContext.kt:63)
    at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(PipelineContext.kt:92)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:45)
    at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(PipelineContext.kt:183)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:142)
    at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(PipelineContext.kt:63)
    at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(PipelineContext.kt:92)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:45)
    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:764)
Caused by: java.lang.RuntimeException: Http Status: 400
    at com.example.api.service.feature.ExampleFeature$Feature$install$1.invokeSuspend(RallyFeature.kt:38)
    at com.example.api.service.feature.ExampleFeature$Feature$install$1.invoke(Unknown Source:40)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:248)
    ... 27 more
wollnyst commented 5 years ago

I have a similar requirement, an easy fix would be to make BadResponseStatusException open instead of final.

This would allow us to to define custom well defined response exceptions, while keeping the current semantic of wrapping other exceptions which might occur in the pipeline.

e5l commented 5 years ago

The fix in master. See HttpResponseValidator feature for the details.