Обработка тайм-аута Spring WebFlux WebClient в Kotlin

Я выполняю HTTP-вызов с помощью Spring WebFlux WebClient (Boot 2.4.3) в Kotlin (1.4.30). Когда время ожидания запроса истекает, происходит сбой из-за исключения, но вместо этого я хотел бы вернуть значение по умолчанию. Я вижу ссылки на onError, onStatus и т. Д., Использованные после retrieve(), но, похоже, они недоступны в моем случае (только body, toEntity, awaitExchange)

Звонок:

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

Конфигурация 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()

Модель ответа:

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

Как я могу вернуть ответ по умолчанию (разговор с пустой картой) в случае тайм-аута вместо сбоя с исключением?

Обновлять:

Я попробовал предложение JArgente ниже: обновил вызов с помощью awaitExchange и установил действительный ответ WireMock с задержкой (1010 мс), которая больше этого тайм-аута (1000 мс).

Результатом остается 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
      }

Ответ:

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

Мок-конфиг для него:

{
  "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"
      }
    }
  ]
}

person Sergei G    schedule 25.05.2021    source источник
comment
При использовании сопрограмм вы можете просто использовать try-catch для обработки соответствующих исключений.   -  person Martin Tarjányi    schedule 25.05.2021


Ответы (2)


Вы получаете исключение, потому что в вашем методе вы ожидаете получить ответ типа Conversation, но поскольку вы получаете сообщение об ошибке, тело отличается. Способ обработки ответа в этом случае должен заключаться в том, чтобы сначала посмотреть код состояния HTTP, а затем соответствующим образом преобразовать тело. Вот пример https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html

В вашем случае, когда вы получите код состояния ошибки, вы должны создать новый пустой разговор и вернуть его

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()
        }
  }
person JArgente    schedule 25.05.2021
comment
Просмотр кода состояния не помогает с таймаутами. Я обновил вопрос результатами вашего предложения. - person Sergei G; 25.05.2021

Согласно предложению Мартина, в итоге просто обернул вызов в 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)

Я думал, что есть какой-то идиоматический способ, но он отлично работает.

person Sergei G    schedule 26.05.2021