Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FeignClient throws instead of returning ResponseEntity with error http status

As I'm using ResponseEntity<T> as return value for my FeignClient method, I was expecting it to return a ResponseEntity with 400 status if it's what the server returns. But instead it throws a FeignException.

How can I get a proper ResponseEntity instead of an Exception from FeignClient ?

Here is my FeignClient:

@FeignClient(value = "uaa", configuration = OauthFeignClient.Conf.class)
public interface OauthFeignClient {

    @RequestMapping(
            value = "/oauth/token",
            method = RequestMethod.POST,
            consumes = MULTIPART_FORM_DATA_VALUE,
            produces = APPLICATION_JSON_VALUE)
    ResponseEntity<OauthTokenResponse> token(Map<String, ?> formParams);

    class Conf {

        @Value("${oauth.client.password}")
        String oauthClientPassword;

        @Bean
        public Encoder feignFormEncoder() {
            return new SpringFormEncoder();
        }

        @Bean
        public Contract feignContract() {
            return new SpringMvcContract();
        }

        @Bean
        public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
            return new BasicAuthRequestInterceptor("web-client", oauthClientPassword);
        }

    }
}

and here how I use it:

@PostMapping("/login")
public ResponseEntity<LoginTokenPair> getTokens(@RequestBody @Valid LoginRequest userCredentials) {
    Map<String, String> formData = new HashMap<>();

    ResponseEntity<OauthTokenResponse> response = oauthFeignClient.token(formData);

    //code never reached if contacted service returns a 400
    ...
}
like image 685
ch4mp Avatar asked Jan 25 '18 11:01

ch4mp


1 Answers

So, looking at source code, it seams that only solution is actually using feign.Response as return type for FeignClient methods and hand decoding the body with something like new ObjectMapper().readValue(response.body().asReader(), clazz) (with a guard on 2xx status of course because for error statuses, it's very likely that body is an error description and not a valid payload ;).

This makes possible to extract and forward status, header, body, etc. even if status is not in 2xx range.

Edit: Here is a way to forward status, headers and mapped JSON body (if possible):

public static class JsonFeignResponseHelper {
    private final ObjectMapper json = new ObjectMapper();

    public <T> Optional<T> decode(Response response, Class<T> clazz) {
        if(response.status() >= 200 && response.status() < 300) {
            try {
                return Optional.of(json.readValue(response.body().asReader(), clazz));
            } catch(IOException e) {
                return Optional.empty();
            }
        } else {
            return Optional.empty();
        }
    }

    public <T, U> ResponseEntity<U> toResponseEntity(Response response, Class<T> clazz, Function<? super T, ? extends U> mapper) {
        Optional<U> payload = decode(response, clazz).map(mapper);

        return new ResponseEntity(
                payload.orElse(null),//didn't find a way to feed body with original content if payload is empty
                convertHeaders(response.headers()),
                HttpStatus.valueOf(response.status()));
    }

    public MultiValueMap<String, String>  convertHeaders(Map<String, Collection<String>> responseHeaders) {
        MultiValueMap<String, String> responseEntityHeaders = new LinkedMultiValueMap<>();
        responseHeaders.entrySet().stream().forEach(e -> 
                responseEntityHeaders.put(e.getKey(), new ArrayList<>(e.getValue())));
        return responseEntityHeaders;
    }
}

that can be used as follow:

@PostMapping("/login")
public ResponseEntity<LoginTokenPair> getTokens(@RequestBody @Valid LoginRequest userCredentials) throws IOException {
    Response response = oauthFeignClient.token();

    return feignHelper.toResponseEntity(
            response,
            OauthTokenResponse.class,
            oauthTokenResponse -> new LoginTokenPair(
                    new BearerToken(oauthTokenResponse.access_token, oauthTokenResponse.expires_in),
                    new BearerToken(oauthTokenResponse.refresh_token, refreshTokenValidity)));
}

This saves headers and status code, but error message is lost :/

like image 57
ch4mp Avatar answered Oct 25 '22 21:10

ch4mp