2

I would like the following method to throw a custom exception if an error occurs:

@Service
public class MyClass {

    private final WebClient webClient;

    public MatcherClient(@Value("${my.url}") final String myUrl) {
        this.webClient = WebClient.create(myUrl);
    }

    public void sendAsync(String request) {

        Mono<MyCustomResponse> result = webClient.post()
            .header(HttpHeaders.CONTENT_TYPE, "application/json")
            .body(BodyInserters.fromObject(request))
            .retrieve()
            .doOnError(throwable -> throw new CustomException(throwable.getMessage()))
            .subscribe(response -> log.info(response));

    }

}

I have also set up a unit test expecting the CustomException to be thrown. Unfortunately the test fails and the Exception is kind of wrapped into a Mono object. Here also the test code for reference:

@Test(expected = CustomException.class)
public void testSendAsyncRethrowingException() {
    MockResponse mockResponse = new MockResponse()
        .setHeader(HttpHeaders.CONTENT_TYPE, "application/json")
        .setResponseCode(500).setBody("Server error");
    mockWebServer.enqueue(mockResponse);

    matcherService.matchAsync(track);
}

I'm using the MockWebServer to mock an error in the test.

So, how should I implement the doOnError or onError part if the call in order to make my method really to throw an exception?

Ira Re
  • 730
  • 3
  • 9
  • 25

3 Answers3

2

I'd advise to expose a reactive API that returns the Mono<Void> from the webclient, especially if you name your method "sendAsync". It's not async if you have to block for the call to return/fail. If you want to provide a sendSync() alternative, you can always make it call sendAsync().block().

For the conversion of exception, you can use the dedicated onErrorMap operator.

For the test, the thing is, you can't 100% test asynchronous code with purely imperative and synchronous constructs (like JUnit's Test(expected=?) annotation). (although some reactive operator don't induce parallelism so this kind of test can sometimes work).

You can also use .block() here (testing is one of the rare occurrences where this is unlikely to be problematic).

But if I were you I'd get in the habit of using StepVerifier from reactor-test. To give an example that sums up my recommendations:

@Service
public class MyClass {

    private final WebClient webClient;

    public MatcherClient(@Value("${my.url}") final String myUrl) {
        this.webClient = WebClient.create(myUrl);
    }

    public Mono<MyCustomResponse> sendAsync(String request) {
        return webClient.post()
            .header(HttpHeaders.CONTENT_TYPE, "application/json")
            .body(BodyInserters.fromObject(request))
            .retrieve()
            .onErrorMap(throwable -> new CustomException(throwable.getMessage()))
            //if you really need to hardcode that logging
            //(can also be done by users who decide to subscribe or further add operators)
            .doOnNext(response -> log.info(response));
    }
}

and the test:

@Test(expected = CustomException.class)
public void testSendAsyncRethrowingException() {
    MockResponse mockResponse = new MockResponse()
        .setHeader(HttpHeaders.CONTENT_TYPE, "application/json")
        .setResponseCode(500).setBody("Server error");
    mockWebServer.enqueue(mockResponse);

    //Monos are generally lazy, so the code below doesn't trigger any HTTP request yet
    Mono<MyCustomResponse> underTest = matcherService.matchAsync(track);

    StepVerifier.create(underTest)
    .expectErrorSatisfies(t -> assertThat(t).isInstanceOf(CustomException.class)
        .hasMessage(throwable.getMessage())
    )
    .verify(); //this triggers the Mono, compares the
               //signals to the expectations/assertions and wait for mono's completion

}
Simon Baslé
  • 27,105
  • 5
  • 69
  • 70
  • Generally that would be an approach if I would start to implement an application from scratch. Unfortunately, I have to deal with running system strictly relying on a custom business exception to be thrown in curtain cases (and creating some reports in catch blocks), so I just have to support the old test with @Test(expected=...). Also the documentation of the reactor project suggests this implementation as a valid option. So thank you for your answer, but this doesn't suit my needs. – Ira Re Oct 08 '19 at 11:12
  • The `expected` attribute of the `@Test` annotation is only required with _JUnit 4_ and is not supported by _JUnit 5_ – Marco Lackovic Mar 16 '21 at 15:05
1

The retrieve() method in WebClient throws a WebClientResponseException whenever a response with status code 4xx or 5xx is received.

1. You can customize the exception using the onStatus() method

public Mono<JSONObject> listGithubRepositories() {
 return webClient.get()
        .uri(URL)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError, clientResponse ->
            Mono.error(new MyCustomClientException())
        )
        .onStatus(HttpStatus::is5xxServerError, clientResponse ->
            Mono.error(new MyCustomServerException())
        )
        .bodyToMono(JSONObject.class);
}

2. Throw the custom exception by checking the response status

   Mono<Object> result = webClient.get().uri(URL).exchange().log().flatMap(entity -> {
        HttpStatus statusCode = entity.statusCode();
        if (statusCode.is4xxClientError() || statusCode.is5xxServerError())
        {
            return Mono.error(new Exception(statusCode.toString()));
        }
        return Mono.just(entity);
    }).flatMap(clientResponse -> clientResponse.bodyToMono(JSONObject.class))

Reference: https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/

Pramod H G
  • 1,513
  • 14
  • 17
0

Instead of using doOnError I swiched to subscribe method accepting also an error consumer:

Mono<MyCustomResponse> result = webClient.post()
            .header(HttpHeaders.CONTENT_TYPE, "application/json")
            .body(BodyInserters.fromObject(request))
            .retrieve()
            .subscribe(response -> log.info(response),
                       throwable -> throw new CustomException(throwable.getMessage()));

This documentation helps a lot: https://projectreactor.io/docs/core/release/reference/index.html#_error_handling_operators

Ira Re
  • 730
  • 3
  • 9
  • 25