简体   繁体   English

处理 Kotlin 中的 Spring WebFlux WebClient 超时

[英]Handle Spring WebFlux WebClient timeout in Kotlin

I am doing a get http call with Spring WebFlux WebClient (Boot 2.4.3) in Kotlin (1.4.30).我正在使用 Kotlin (1.4.30) 中的 Spring WebFlux WebClient (Boot 2.4.3) 调用 http。 When request times out it fails with exception but instead I'd like to return a default value.当请求超时时,它会因异常而失败,但我想返回一个默认值。 I see references to onError , onStatus etc. used after retrieve() but they don't seem to be available in my case (only body , toEntity , awaitExchange )我看到在 restore retrieve()之后使用了对onErroronStatus等的引用,但在我的情况下它们似乎不可用(只有bodytoEntityawaitExchange

The call:来电:

suspend fun conversation(id: String): Conversation =
    client.get().uri("/conversation/{id}", id).retrieve().awaitBody()

WebClient configuration with connect and read timeouts:带有连接和读取超时的 WebClient 配置:

fun webClient(url: String, connectTimeout: Int, readTimeout: Long, objectMapper: ObjectMapper): WebClient =
    WebClient.builder()
      .baseUrl(url)
      .exchangeStrategies(
        ExchangeStrategies.builder()
          .codecs { configurer -> configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) }
          .build())
      .clientConnector(
        ReactorClientHttpConnector(
          HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
            .doOnConnected { connection ->
              connection.addHandlerLast(ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS))
            }))
      .build()

Response model:响应 model:

@JsonIgnoreProperties(ignoreUnknown = true)
data class Conversation(
  val replyTimestamp: Map<String, String>,
)

How can I return default response (conversation with empty map) in case of timeout instead of failing with an exception?如何在超时而不是因异常而失败时返回默认响应(与空地图的对话)?

Update:更新:

I tried suggestion of JArgente below: updated the call with awaitExchange and set valid WireMock response with delay (1010 ms) that is longer that timeout (1000 ms).我在下面尝试了 JArgente 的建议:使用awaitExchange更新了调用,并设置了有效的 WireMock 响应,延迟时间(1010 毫秒)比超时时间(1000 毫秒)更长。

Result is still ReadTimeoutException so looking at http status code does not help in this case.结果仍然是 ReadTimeoutException 所以查看 http 状态代码在这种情况下没有帮助。

  private val defaultConversation = Conversation(emptyMap())

  suspend fun conversation(id: String): Conversation =
    client.get()
      .uri("/conversation/{id}", id)
      .awaitExchange {
          response -> if (response.statusCode() == HttpStatus.OK)  response.awaitBody() else defaultConversation
      }

Response:回复:

{
  "replyTimestamp": {
    "1": "2021-02-23T15:30:28.753Z",
    "2": "2021-02-23T16:30:28.753Z"
  }
}

Mock config for it:模拟配置:

{
  "mappings":
  [
    {
      "priority": 1,
      "request": {
        "method": "GET",
        "urlPathPattern": "/conversation/1"
      },
      "response": {
        "status": 200,
        "fixedDelayMilliseconds": 1010,
        "headers": {
          "content-type": "application/json;charset=utf-8"
        },
        "bodyFileName": "conversation1.json"
      }
    }
  ]
}

You are getting an exception because in your method, you are expecting to get a response of type Conversation, but because you are receiving an error, the body is different.你得到一个异常是因为在你的方法中,你期望得到一个对话类型的响应,但是因为你收到一个错误,所以正文是不同的。 The way you should handle the response, in this case, should be first, to look at the HTTP status code and then convert the body accordingly.在这种情况下,您应该处理响应的方式应该是首先查看 HTTP 状态代码,然后相应地转换正文。 Here is an example from spring.io https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html Here is an example from spring.io https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html

In your case, when you receive the status code of the error you should create a new empty Conversation and return it在您的情况下,当您收到错误的状态代码时,您应该创建一个新的空对话并返回它

val entity = client.get()
  .uri("/conversation/{id}", id)
  .accept(MediaType.APPLICATION_JSON)
  .awaitExchange {
        if (response.statusCode() == HttpStatus.OK) {
             return response.awaitBody<Conversation>()
        }
        else if (response.statusCode().is4xxClientError) {
             return response.awaitBody<ErrorContainer>()
        }
        else {
             throw response.createExceptionAndAwait()
        }
  }

As per Martin's suggestion ended up just wrapping call in a try/catch:根据 Martin 的建议,最终只是将调用包装在 try/catch 中:

suspend inline fun <reified T : Any> WebClient.ResponseSpec.tryAwaitBodyOrElseLogged(default: T, log: Logger) : T =
  try {
    awaitBody()
  } catch (e: Exception) {
    log.warn("Remote request failed, returning default value ($default)", e)
    default
  }
private val log = LoggerFactory.getLogger(this::class.java)
private val default = Conversation(emptyMap())

suspend fun conversation(id: String): Conversation =
  client.get()
    .uri("/conversation/{id}", id)
    .retrieve()
    .tryAwaitBodyOrElseLogged(default, log)

I thought there is some idiomatic way but this works fine.我认为有一些惯用的方式,但这很好用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM