Beware of using spring WebClient(Episode2)

When we use exchangeToMono

I explained the WebClient exchange operator in episode1 article. However exchange operator is deprecated so Instead of using exchange, we have to use exchangeToMono(or exchangeToFlux)

1) doOnSuccess and onErrorResume combination

.exchangeToMono(clientResponse -> clientResponse.toEntity(String.class))
.doOnSuccess(clientResponse -> {
    if (clientResponse.getStatusCode() != HttpStatus.OK) {
      throw new RuntimeException("error");
    }
  }
)
.onErrorResume(error -> {
  return Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
});

If the responding server returns 4xx or 5xx status code due to the business policy, doOnSuccess will receive the response and Because status code will not be OK(200), It will throw a RuntimeException. Finally The response returns INTERNAL_SERVER_ERROR(500) from onErrorResume.

However, if there is a network communication problem, it will return 5xx by going directly to onErrorResume without going through doOnSuccess. For example reactor.netty.http.client.PrematureCloseException: Connection prematurely closed DURING response error or Connection reset by peer error

cf) Personally Due to the doOnSuccess naming, I predicted only OK(200) can receive the response at first. However doOnSuccess will receive the response whether it is 4xx or 5xx, except network communication error.

※Beware

I said that doOnSuccess will receive the response no matter what is 4xx or 5xx, If we get a proper response. However, this is based on the premise that we use exchangeToMono or exchangeToFlux operator. If we receive a response using retrieve as below, the 4xx and 5xx responses do not go to doOnSuccess.

.retrieve()
.bodyToMono(String.class)
.doOnSuccess(
  responseBody -> {
    System.out.println("responseBody : " + responseBody);
  }
)

2) exchangeToMono and onErrorResume combination

.exchangeToMono(clientResponse -> {
  if (clientResponse.rawStatusCode() == HttpStatus.BAD_REQUEST.value()) {
    return clientResponse
      .createException()
      .flatMap(it -> Mono.error(new RuntimeException("400!!")));
  }

  if (clientResponse.rawStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
    return clientResponse
      .createException()
      .flatMap(it -> Mono.error(new RuntimeException("500!!")));
  }

  return clientResponse.bodyToMono(String.class);
})
.onErrorResume(error -> {
  return Mono.error(new RuntimeException(error.getMessage()));
});

If the responding server returns a 4xx or 5xx error due to the business policy, it returns a RuntimeException is wrapped Mono.error. Then, onErrorResume receive again and return Mono.error.

If we write the code as above, we must create a ExceptionHandler that receives RuntimeException

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public String runtimeExceptionHandler(RuntimeException ex) {
  return "runtimeException";
}

or additional appropriate exception handling is required on the receiving side of the webClient response.

If reactor.netty.http.client.PrematureCloseException: Connection prematurely closed DURING response error occurs, response goes to onErrorResume while operating inside the exchangeToMono lambda body. If Connection reset by peer error occurs, response goes straight to onErrorResume.

※Beware

When clientResponse.rawStatusCode() is 400 or 500 status code, the response go to the onErrorResume because we returns a RuntimeException is wrapped Mono.error using clientResponse.createException().flatMap operator. Don't think 400 or 500 status code will go to the onErrorResume unconditionally.

If we return the ResponseEntity that filled with response header and response body as shown in the code below, it will not go to the onErrorResume even if 400 or 500 response is received.

.exchangeToMono(clientResponse -> {
  // return ResponseEntity
  return clientResponse.toEntity(String.class);
})
.onErrorResume(throwable -> {
  return Mono.just(new ResponseEntity<>("HAHAHA", HttpStatus.INTERNAL_SERVER_ERROR));
});

3) retrieve and onErrorResume combination

Spring maintainers recommend to use retrieve operator. Because using the exchange operator can cause memory leak or connection leak when we requires direct processing of responses.

.retrieve()
.onStatus(
  httpStatus -> httpStatus != HttpStatus.OK,
    clientResponse -> {
      return clientResponse.createException()
         .flatMap(it -> Mono.error(new RuntimeException("code : " + clientResponse.statusCode())));
      })
  .bodyToMono(String.class)
  .onErrorResume(throwable -> {
    return Mono.error(new RuntimeException(throwable));
});

So, if we change it to the retrieve operator, you can write the code as above, and the flow by response situation is not different.