Spring WebClient - 如何在 HTTP 错误(4xx、5xx)的情况下访问响应正文?

Posted

技术标签:

【中文标题】Spring WebClient - 如何在 HTTP 错误(4xx、5xx)的情况下访问响应正文?【英文标题】:Spring WebClient - how to access response body in case of HTTP errors (4xx, 5xx)? 【发布时间】:2020-06-29 18:53:59 【问题描述】:

我想将我的异常从“数据库”REST API 重新抛出到“后端”REST API,但我丢失了原始异常的消息。

这是我通过 Postman 从我的“数据库”REST API 中得到的:


    "timestamp": "2020-03-18T15:19:14.273+0000",
    "status": 400,
    "error": "Bad Request",
    "message": "I'm DatabaseException (0)",
    "path": "/database/api/vehicle/test/0"

这部分没问题。

这是我通过 Postman 从我的“后端”REST API 中得到的:


    "timestamp": "2020-03-18T15:22:12.801+0000",
    "status": 400,
    "error": "Bad Request",
    "message": "400 BAD_REQUEST \"\"; nested exception is org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from GET http://localhost:8090/database/api/vehicle/test/0",
    "path": "/backend/api/vehicle/test/0"

如您所见,原来的“消息”字段丢失了。

我用:

Spring Boot 2.2.5.RELEASE spring-boot-starter-web spring-boot-starter-webflux

后端和数据库以 Tomcat (web and webflux in the same application) 开头。

这是后端:

    @GetMapping(path = "/test/id")
    public Mono<Integer> test(@PathVariable String id) 
        return vehicleService.test(id);
    

使用vehicleService.test:

    public Mono<Integer> test(String id) 
        return WebClient
            .create("http://localhost:8090/database/api")
            .get()
            .uri("/vehicle/test/id", id)
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(Integer.class);
    

这是数据库:

    @GetMapping(path = "/test/id")
    public Mono<Integer> test(@PathVariable String id) throws Exception 

        if (id.equals("0")) 
            throw new DatabaseException("I'm DatabaseException (0)");
        

        return Mono.just(Integer.valueOf(2));
    

我也试过return Mono.error(new DatabaseException("I'm DatabaseException (0)"));

还有数据库异常:

public class DatabaseException extends ResponseStatusException 

    private static final long serialVersionUID = 1L;

    public DatabaseException(String message) 
        super(HttpStatus.BAD_REQUEST, message);

    

似乎我的后端转换了响应,在互联网上找不到任何答案。

【问题讨论】:

尝试返回Mono.error(new RestStatusException(HttpStatus.BAD_REQUEST, "I'm DatabaseException (0)")); 我已经尝试过了,但它不起作用。 【参考方案1】:

您可以使用 exchange 代替 retrieveWebClient,它可以让您处理错误并使用从服务响应中检索到的消息传播自定义异常。

private void execute()

    WebClient webClient = WebClient.create();

    webClient.get()
             .uri("http://localhost:8089")
             .exchangeToMono(this::handleResponse)
             .doOnNext(System.out::println)
             .block();  // not required, just for testing purposes


private Mono<Response> handleResponse(ClientResponse clientResponse)

    if (clientResponse.statusCode().isError())
    
        return clientResponse.bodyToMono(Response.class)
                             .flatMap(response -> Mono.error(new RuntimeException(response.message)));
    

    return clientResponse.bodyToMono(Response.class);


private static class Response

    private String message;

    public Response()
    
    

    public String getMessage()
    
        return message;
    

    public void setMessage(String message)
    
        this.message = message;
    

    @Override
    public String toString()
    
        return "Response" +
                "message='" + message + '\'' +
                '';
    

【讨论】:

当我在 setMessage 中设置断点时,我可以看到我原来的消息错误!现在我要弄清楚如何抛出这个错误并停止通量,以及当没有错误时如何发送整数值。 With .flatMap(response -> Mono.error(new ResponseStatusException(...)));我的状态为 400,如果我使用任何其他类型的异常,我的状态为 500,我的状态丢失了。 我猜,您已经假设错误和成功案例的响应都是相同的。如果我错了,请纠正我。【参考方案2】:

下面的代码现在可以工作了,这是我原来的问题之外的另一个代码,但它的想法几乎相同(使用后端 REST api 和数据库 REST api)。

我的数据库 REST api:

@RestController
@RequestMapping("/user")
public class UserControl 

    @Autowired
    UserRepo userRepo;

    @Autowired
    UserMapper userMapper;

    @GetMapping("/login")
    public Mono<UserDTO> getUser(@PathVariable String login) throws DatabaseException 
        User user = userRepo.findByLogin(login);
        if(user == null) 
            throw new DatabaseException(HttpStatus.BAD_REQUEST, "error.user.not.found");
        
        return Mono.just(userMapper.toDTO(user));
    

UserRepo 只是一个@RestReporitory。

UserMapper 使用 MapStruct 将我的实体映射到 DTO 对象。

与:

@Data
@EqualsAndHashCode(callSuper=false)
public class DatabaseException extends ResponseStatusException 

    private static final long serialVersionUID = 1L;

    public DatabaseException(String message) 
        super(HttpStatus.BAD_REQUEST, message);
    

@Data & EqualsAndHashCode 来自 Lombok 库。

Extends ResponseStatusException 在这里非常重要,如果你不这样做,那么响应将被错误处理。

我的后端 REST api 从数据库 REST API 接收数据:

@RestController
@RequestMapping("/user")
public class UserControl 

    @Value("$database.api.url")
    public String databaseApiUrl;

    private String prefixedURI = "/user";

    @GetMapping("/login")
    public Mono<UserDTO> getUser(@PathVariable String login) 
        return WebClient
                .create(databaseApiUrl)
                .get()
                .uri(prefixedURI + "/login", login).retrieve()
                .onStatus(HttpStatus::isError, GlobalErrorHandler::manageError)
                .bodyToMono(UserDTO.class);
    

使用 GlobalErrorHandler::

public class GlobalErrorHandler 

    /**
     * Translation key for i18n
     */
    public final static String I18N_KEY_ERROR_TECHNICAL_EXCEPTION = "error.technical.exception";

    public static Mono<ResponseStatusException> manageError(ClientResponse clientResponse) 

        if (clientResponse.statusCode().is4xxClientError()) 
            // re-throw original status and message or they will be lost
            return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> 
                return Mono.error(new ResponseStatusException(response.getStatus(), response.getMessage()));
            );
         else  // Case when it's 5xx ClientError
            // User doesn't have to know which technical exception has happened
            return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> 
                return Mono.error(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
                        I18N_KEY_ERROR_TECHNICAL_EXCEPTION));
            );
        

    

而 ExceptionResponseDTO 是从 clientResponse 检索一些数据所必需的:

/**
 * Used to map <a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ClientResponse.html">ClientResponse</a> from WebFlux 
 */
@Data
@EqualsAndHashCode(callSuper=false)
public class ExceptionResponseDTO extends Exception 

    private static final long serialVersionUID = 1L;

    private HttpStatus status;

    public ExceptionResponseDTO(String message) 
        super(message);
    

    /**
     * Status has to be converted into @link HttpStatus
     */
    public void setStatus(String status) 
        this.status = HttpStatus.valueOf(Integer.valueOf(status));
    


另一个可能有用的相关类:ExchangeFilterFunctions.java

我在这个问题上找到了很多信息:

https://github.com/spring-projects/spring-framework/issues/20280

即使这些信息是旧的,它们仍然是相关的!

【讨论】:

以上是关于Spring WebClient - 如何在 HTTP 错误(4xx、5xx)的情况下访问响应正文?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring 响应式 WebClient 中返回 Kotlin Coroutines Flow

如何使用Spring WebClient同时进行多个调用?

Spring 5 webflux如何在Webclient上设置超时

如何在运行在 Tomcat 上的 Spring Web 应用程序中使用 Spring 的响应式 WebClient

如何在 Spring 5 WebFlux WebClient 中设置超时

如何从 Spring WebClient 获取响应 json