Spring WebClient - How to handle error scenarios
Asked Answered
W

3

7

We're using org.springframework.web.reactive.function.client.WebClient with reactor.netty.http.client.HttpClient as part of Spring 5.1.9 to make requests using the exchange() method. The documentation for this method highlights the following:

... when using exchange(), it is the responsibility of the application to consume any response content regardless of the scenario (success, error, unexpected data, etc). Not doing so can cause a memory leak.

Our use of exchange() is rather basic, but the documentation for error scenarios is unclear to me and I want to be certain that we are correctly releasing resources for all outcomes. In essence, we have a blocking implementation which makes a request and returns the ResponseEntity regardless of the response code:

    try {
        ...
        ClientResponse resp = client.method(method).uri(uri).syncBody(body).exchange().block();
        ResponseEntity<String> entity =  resp.toEntity(String.class).block();
        return entity;
    } catch (Exception e) {
        // log error details, return internal server error
    }

If I understand the implementation, exchange() will always give us a response if the request was successfully dispatched, regardless of response code (e.g. 4xx, 5xx). In that scenario, we just need to invoke toEntity() to consume the response. My concern is for error scenarios (e.g. no response, low-level connection errors, etc). Will the above exception handling catch all other scenarios and will any of them have a response that needs to be consumed?

Note: ClientResponse.releaseBody() was only introduced in 5.2

Wrinkly answered 19/2, 2020 at 16:20 Comment(0)
F
12

The response have to be consumed when the request was made, but if you can't do the request probably an exception was be throwed before, and you will no have problems with response.

In the documentation says:

NOTE: When using a ClientResponse through the WebClient exchange() method, you have to make sure that the body is consumed or released by using one of the following methods:

  1. body(BodyExtractor)
  2. bodyToMono(Class) or bodyToMono(ParameterizedTypeReference)
  3. bodyToFlux(Class) or bodyToFlux(ParameterizedTypeReference)
  4. toEntity(Class) or toEntity(ParameterizedTypeReference)
  5. toEntityList(Class) or toEntityList(ParameterizedTypeReference)
  6. toBodilessEntity()
  7. releaseBody()

You can also use bodyToMono(Void.class) if no response content is expected. However keep in mind the connection will be closed, instead of being placed back in the pool, if any content does arrive. This is in contrast to releaseBody() which does consume the full body and releases any content received.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ClientResponse.html

You can try to use .retrieve() instead .exchange() and handle errors as your preference.

  public Mono<String> someMethod() {

    return webClient.method(method)
      .uri(uri)
      .retrieve()
      .onStatus(
        (HttpStatus::isError), // or the code that you want
        (it -> handleError(it.statusCode().getReasonPhrase())) //handling error request
      )
      .bodyToMono(String.class);


  }

  private Mono<? extends Throwable> handleError(String message) {
    log.error(message);
    return Mono.error(Exception::new);
  }

In this example I used Exception but you can create some exception more specific and then use some exception handler to return the http status that you want. Is not recommended to use block, a better way is pass the stream forward.

Foti answered 14/3, 2020 at 23:41 Comment(0)
F
0

create some exception classes

Autowired ObjectMapper

Create a method that returns Throwable

Create a custom class for Error.

return webClient
                .get()
                .uri(endpoint)
                .retrieve()
                .bodyToMono(Model.class)
                .onErrorMap(WebClientException.class, this::handleHttpClientException);



private Throwable handleHttpClientException(Throwable ex) {
            if (!(ex instanceof WebClientResponseException)) {
                LOG.warn("Got an unexpected error: {}, will rethrow it", ex.toString());
                return ex;
            }
    
            WebClientResponseException wcre = (WebClientResponseException)ex;
    
            switch (wcre.getStatusCode()) {
                case NOT_FOUND -> throw new NotFoundException(getErrorMessage(wcre));
                case BAD_REQUEST -> throw new BadRequestException(getErrorMessage(wcre));
                default -> {
                    LOG.warn("Got a unexpected HTTP error: {}, will rethrow it", wcre.getStatusCode());
                    LOG.warn("Error body: {}", wcre.getResponseBodyAsString());
                    return ex;
                }
            }
        }



private String getErrorMessage(WebClientResponseException ex) {
        try {
            return mapper.readValue(ex.getResponseBodyAsString(), HttpErrorInfo.class).getMessage();
        } catch (IOException ioe) {
            return ex.getMessage();
        }
    }
Flex answered 19/4, 2021 at 6:50 Comment(0)
K
0

If you want to call another endpoint (non-blocking) reactively from inside your controller end point service method then this is how i do it.

This scenario calls an add token for an AssetServer and InventoryServer endpoint respectively. Spring will auto call the constructor of your service as follows.

    private final WebClient inventoryClient;
private final WebClient assetClient;

public UserService(Environment env, WebClient.Builder builder) {
    inventoryClient = builder.baseUrl(Objects.requireNonNull(env.getProperty("server.inventory.url"))).build();
    assetClient = builder.baseUrl(Objects.requireNonNull(env.getProperty("server.asset.url"))).build();
}

The endpoint urls are defined in application.properties

# SERVER URLs

server.inventory.url=http://localhost:3601 server.asset.url=http://localhost:3602

The test endpoint can be setup like this in your controller java class

    @GetMapping("/test")
@ResponseStatus(HttpStatus.OK)
public Mono<ResponseEntity<UserInfo>> test() {
    return userService
            .test()
            .map(response -> ResponseEntity.ok().body(response))
            .doOnSuccess(r -> log.info("{} [ http status = {} ]", "Test", HttpStatus.OK))
            .doOnError(error -> log.error("{} [ status = {}  message = {}  ]",
                    "Test", HttpStatus.BAD_REQUEST, error.getMessage()))
            .onErrorReturn(ResponseEntity.badRequest().build());
}

Your test method can be called in your service like this

    public Mono<UserInfo> test() {
    return CallInventory()
            .then(CallAsset())
            .then(loginUser())
            .onErrorResume(error -> Mono.just(UserInfo.builder().status(AUTHENTICATION_FAILED).build()));
}

The two webclient endpoint are put into private methods as follows

    private Mono<Void> CallInventory()
{
    return inventoryClient.post()
            .uri("/inventory/token/addToken/dfgdfgdfgdfg")
            .exchangeToMono(Mono::just)
            .onErrorMap(t -> new RuntimeException("Inventory server is down"))
            .then();
}

private Mono<Void> CallAsset()
{
    return assetClient.post()
            .uri("/asset/token/addToken/dfgdfgdfgdfg")
            .exchangeToMono(Mono::just)
            .onErrorMap(t -> new RuntimeException("Asset server is down"))
            .then();
}

The main login login could be placed here

    private Mono<UserInfo> loginUser()
{
    return Mono.just(UserInfo.builder().token("ddd").build());
}

Summary

The test() method simply chains the call to asset server then a call to the inventory server. If the connection is down it will cause the onErrorMap to be thrown which here throw a RuntimeException with an error message. The calling test() method catches this reactively in the OnErrorResume and sends back whatever mono result you want.

You should be able to adapt this example for different usages.

Kalakalaazar answered 22/7 at 19:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.