Как вызвать исключение в части ошибки реактивного вызова Spring WebClient?

Я хотел бы, чтобы следующий метод генерировал настраиваемое исключение в случае возникновения ошибки:

@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));

    }

}

Я также настроил модульный тест, ожидая выброса CustomException. К сожалению, тест не проходит, и исключение как бы обернуто в объект Mono. Вот также тестовый код для справки:

@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);
}

Я использую MockWebServer, чтобы имитировать ошибку в тесте.

Итак, как мне реализовать часть doOnError или onError, если вызов, чтобы мой метод действительно генерировал исключение?




Ответы (3)


Я бы посоветовал предоставить реактивный API, который возвращает Mono<Void> от веб-клиента, особенно если вы назовете свой метод "sendAsync". Это не асинхронно, если вам нужно заблокировать вызов для возврата / сбоя. Если вы хотите предоставить sendSync() альтернативу, вы всегда можете вызвать sendAsync().block().

Для преобразования исключения вы можете использовать специальный оператор onErrorMap.

Что касается теста, дело в том, что вы не можете на 100% протестировать асинхронный код с чисто императивными и синхронными конструкциями (например, аннотацией Test(expected=?) JUnit). (хотя некоторые реактивные операторы не вызывают параллелизма, поэтому такой тест может иногда работать).

Вы также можете использовать .block() здесь (тестирование - одно из тех редких случаев, когда это вряд ли вызовет проблемы).

Но на вашем месте я бы привык использовать StepVerifier из reactor-test. Приведу пример, обобщающий мои рекомендации:

@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));
    }
}

и тест:

@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

}
person Simon Baslé    schedule 03.10.2019
comment
Как правило, это был бы подход, если бы я начал реализовывать приложение с нуля. К сожалению, мне приходится иметь дело с работающей системой, строго полагающейся на настраиваемое бизнес-исключение, которое будет выдаваться в шторках (и создание некоторых отчетов в блоках catch), поэтому мне просто нужно поддерживать старый тест с помощью @Test (expected = ... ). Также документация по проекту реактора предлагает эту реализацию как допустимый вариант. Так что спасибо за ваш ответ, но это не соответствует моим потребностям. - person Ira Re; 08.10.2019
comment
Атрибут expected аннотации @Test требуется только в JUnit 4 и не поддерживается в JUnit 5 - person Marco Lackovic; 16.03.2021

Метод retrieve () в WebClient вызывает исключение WebClientResponseException всякий раз, когда получен ответ с кодом состояния 4xx или 5xx.

1. Вы можете настроить исключение с помощью метода onStatus ()

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. Вызвать настраиваемое исключение, проверив статус ответа

   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))

Ссылка: https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/

person Pramod H G    schedule 05.05.2021

Вместо использования doOnError я переключился на метод подписки, принимающий также потребителя ошибки:

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()));

Эта документация очень помогает: https://projectreactor.io/docs/core/release/reference/index.html#_error_handling_operators

person Ira Re    schedule 02.10.2019