Dimitri
Dimitri

Reputation: 81

Spring WebClient - how to access response body in case of HTTP errors (4xx, 5xx)?

I want to re-throw my exception from my "Database" REST API to my "Backend" REST API but I lose the original exception's message.

This is what i get from my "Database" REST API via Postman:

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

This part is ok.

This is what i get from my "Backend" REST API via Postman:

{
    "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"
}

As you can see the original "message" field is lost.

I use:

Backend and Database start with Tomcat (web and webflux in the same application).

This is Backend:

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

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

This is Database:

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

I also tried with return Mono.error(new DatabaseException("I'm DatabaseException (0)"));

And DatabaseException:

public class DatabaseException extends ResponseStatusException {

    private static final long serialVersionUID = 1L;

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

    }
}

It seems my Backend transforms the response and can't find any answer on internet.

Upvotes: 3

Views: 19645

Answers (2)

Martin Tarj&#225;nyi
Martin Tarj&#225;nyi

Reputation: 9947

Instead of retrieve of WebClient, you could use exchange which lets you handle the error and propagate a custom exception with a message retrieved from the service response.

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 + '\'' +
                '}';
    }
}

Upvotes: 6

Dimitri
Dimitri

Reputation: 81

Code below is now working, it's another code than my original question but it's pretty much the same idea (with Backend REST api and Database REST api).

My Database 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 is just a @RestReporitory.

UserMapper use MapStruct to map my Entity to DTO object.

With :

@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 come from Lombok library.

Extends ResponseStatusException is very important here, if you don't do that then response will be bad handled.

My Backend REST api which receives data from Database 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);
    }
}

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

    }
}

And ExceptionResponseDTO which is mandatory to retrieve some data from 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));
    }

}

One other related class which could be useful: ExchangeFilterFunctions.java

I found a lot of information in this issue:

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

Even if these information are old they are still relevent !

Upvotes: 5

Related Questions