Spring Webflux: Webclient:获取错误响应体

52

我正在使用Spring WebFlux中的WebClient,代码如下:

WebClient.create()
            .post()
            .uri(url)
            .syncBody(body)
            .accept(MediaType.APPLICATION_JSON)
            .headers(headers)
            .exchange()
            .flatMap(clientResponse -> clientResponse.bodyToMono(tClass));

它运作良好。我现在想处理我调用的Web服务的错误(例如500内部错误)。通常情况下,我会在“stream”上添加一个“doOnError”并使用Throwable来测试状态码。

但我的问题是,我想获取Web服务提供的正文,因为它提供了我想使用的消息。

我希望无论发生什么都能够执行flatMap,并测试自己的状态码以序列化或不序列化正文。


1
嘿,你得到答案了吗?我也想从另一个服务获取错误响应体,该响应体与成功响应体不同。 - Samra
我有一个类似的问题,即我从WebClient收到了一个错误响应(422),错误响应中有一个具有某些JSON内容的错误代码的主体,我需要检索它。我在这里找不到任何帮助(尽管可能是因为我不够理解?),但是我发现确实从这个教程中得到了帮助: https://medium.com/nerd-for-tech/webclient-error-handling-made-easy-4062dcf58c49 - Bill Naylor
14个回答

38

我更喜欢使用ClientResponse提供的方法来处理HTTP错误并抛出异常:

WebClient.create()
         .post()
         .uri( url )
         .body( bodyObject == null ? null : BodyInserters.fromValue( bodyObject ) )
         .accept( MediaType.APPLICATION_JSON )
         .headers( headers )
         .exchange()
         .flatMap( clientResponse -> {
             //Error handling
             if ( clientResponse.statusCode().isError() ) { // or clientResponse.statusCode().value() >= 400
                 return clientResponse.createException().flatMap( Mono::error );
             }
             return clientResponse.bodyToMono( clazz )
         } )
         //You can do your checks: doOnError (..), onErrorReturn (..) ...
         ...

事实上,这是在DefaultWebClient的DefaultResponseSpec中处理错误时使用的相同逻辑。如果我们使用retrieve()而不是exchange(),那么DefaultResponseSpec就是ResponseSpec的一种实现。


1
太棒了!非常感谢。这是完美的解决方案。虽然有很多使用过滤器、全局异常处理程序等的变通方法,但这个方法完美地解决了问题。对我来说,我想在subscribe中对数据库执行一些操作..它运行得非常好! - Winster
嗨,在新版本的Spring Webflux中,方法exchange已被弃用,我该如何使用retrieve解决方案? - Jesus
@Jesus 你可以使用 exchangeToMono 或 exchangeToFlux... - Zrom

35

我们没有 onStatus() 函数吗?

    public Mono<Void> cancel(SomeDTO requestDto) {
        return webClient.post().uri(SOME_URL)
                .body(fromObject(requestDto))
                .header("API_KEY", properties.getApiKey())
                .retrieve()
                .onStatus(HttpStatus::isError, response -> {
                    logTraceResponse(log, response);
                    return Mono.error(new IllegalStateException(
                            String.format("Failed! %s", requestDto.getCartId())
                    ));
                })
                .bodyToMono(Void.class)
                .timeout(timeout);
    }

而且:

    public static void logTraceResponse(Logger log, ClientResponse response) {
        if (log.isTraceEnabled()) {
            log.trace("Response status: {}", response.statusCode());
            log.trace("Response headers: {}", response.headers().asHttpHeaders());
            response.bodyToMono(String.class)
                    .publishOn(Schedulers.elastic())
                    .subscribe(body -> log.trace("Response body: {}", body));
        }
    }

6
onStatus 很酷,但它存在一个缺陷,即空响应体将规避 response -> { } Lambda 表达式。也就是说,不会返回 Mono.error,而是返回空的 Mono。 - Joe B
在这里,我们总是有一个主体。没有主体的话,500可能会有点不寻常吧? - WesternGun

23

我这样做得到了错误信息:

webClient
...
.retrieve()    
.onStatus(HttpStatus::isError, response -> response.bodyToMono(String.class) // error body as String or other class
                                                   .flatMap(error -> Mono.error(new RuntimeException(error)))) // throw a functional exception
.bodyToMono(MyResponseType.class)
.block();

20

你也可以这样做

return webClient.getWebClient()
 .post()
 .uri("/api/Card")
 .body(BodyInserters.fromObject(cardObject))
 .exchange()
 .flatMap(clientResponse -> {
     if (clientResponse.statusCode().is5xxServerError()) {
        clientResponse.body((clientHttpResponse, context) -> {
           return clientHttpResponse.getBody();
        });
     return clientResponse.bodyToMono(String.class);
   }
   else
     return clientResponse.bodyToMono(String.class);
});

阅读这篇文章以获取更多示例链接,当我遇到类似错误处理问题时,我发现它非常有帮助。


花了一整天的时间才找到这个答案。完全忘记了异常嵌入在响应体中。谢谢! - Blake Neal
在遇到5xx服务器错误时,我们如何抛出异常并打印后端响应? - Rocky4Ever
@Rocky4Ever,你可以抛出一个异常而不是返回成功响应。请参阅以下响应: https://dev59.com/tFcP5IYBdhLWcg3wTYUn#44593201 - Mohale

8
我会像这样做:

我会执行以下操作:

Mono<ClientResponse> responseMono = requestSpec.exchange()
            .doOnNext(response -> {
                HttpStatus httpStatus = response.statusCode();
                if (httpStatus.is4xxClientError() || httpStatus.is5xxServerError()) {
                    throw new WebClientException(
                            "ClientResponse has erroneous status code: " + httpStatus.value() +
                                    " " + httpStatus.getReasonPhrase());
                }
            });

接下来:

responseMono.subscribe(v -> { }, ex -> processError(ex));

在我们这边无法工作,如果出现服务器错误,我们从未进入doOnNext。我们尝试使用doOnEach,但是我们无法从那里获取正文。 - adrien le roy
你们在使用哪个应用服务器?我们这边是用的Netty。 - adrien le roy

8

6

借鉴了这篇关于“使用 Reactor 正确抛出异常的方法”的绝妙 Stack Overflow 答案,我成功地完成了这个答案。它使用.onStatus.bodyToMono.handle 将错误响应体映射到异常。

// create a chicken
webClient
    .post()
    .uri(urlService.getUrl(customer) + "/chickens")
    .contentType(MediaType.APPLICATION_JSON)
    .body(Mono.just(chickenCreateDto), ChickenCreateDto.class) // outbound request body
    .retrieve()
    .onStatus(HttpStatus::isError, clientResponse ->
        clientResponse.bodyToMono(ChickenCreateErrorDto.class)
            .handle((error, sink) -> 
                sink.error(new ChickenException(error))
            )
    )
    .bodyToMono(ChickenResponse.class)
    .subscribe(
            this::recordSuccessfulCreationOfChicken, // accepts ChickenResponse
            this::recordUnsuccessfulCreationOfChicken // accepts throwable (ChickenException)
    );

5
最新的Spring版本,它将HttpStatus更改为HttpStatusCode,他们这样做浪费了我30分钟 ‍♂️。 - Anand Rockzz

4

我曾经遇到类似的情况,发现webClient即使收到4xx/5xx响应也不会抛出任何异常。在我的情况下,我使用webclient首先进行一次调用以获取响应,如果返回2xx响应,则从响应中提取数据并用于进行第二次调用。如果第一次调用收到了非2xx响应,则抛出异常。由于它没有抛出异常,所以当第一次调用失败时,第二次调用仍然会继续进行。因此,我所做的是

return webClient.post().uri("URI")
    .header(HttpHeaders.CONTENT_TYPE, "XXXX")
    .header(HttpHeaders.ACCEPT, "XXXX")
    .header(HttpHeaders.AUTHORIZATION, "XXXX")
    .body(BodyInserters.fromObject(BODY))
    .exchange()
    .doOnSuccess(response -> {
        HttpStatus statusCode = response.statusCode();
        if (statusCode.is4xxClientError()) {
            throw new Exception(statusCode.toString());
        }
        if (statusCode.is5xxServerError()) {
            throw new Exception(statusCode.toString());
        }
    )
    .flatMap(response -> response.bodyToMono(ANY.class))
    .map(response -> response.getSomething())
    .flatMap(something -> callsSecondEndpoint(something));
}

3
我们终于明白发生了什么:默认情况下,Netty的httpclient(HttpClientRequest)被配置为在服务器错误(响应5XX)而不是客户端错误(4XX)时失败,这就是为什么它总是发出异常的原因。
我们所做的是扩展AbstractClientHttpRequest和ClientHttpConnector,以配置httpclient的行为方式并在调用WebClient时使用我们的自定义ClientHttpConnector:
 WebClient.builder().clientConnector(new CommonsReactorClientHttpConnector()).build();

2

WebClient的retrieve()方法在接收到状态码为4xx或5xx的响应时会抛出WebClientResponseException异常。

您可以通过检查响应状态码来处理异常。

   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/ 这个网站提供了关于Spring 5中的Reactive WebClient和WebTestClient的示例。这些示例可以帮助你理解如何使用这些工具来编写响应式的客户端代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接