FeignClient throws instead of returning ResponseEntity with error http status

后端 未结 2 1940
谎友^
谎友^ 2020-12-18 11:35

As I\'m using ResponseEntity as return value for my FeignClient method, I was expecting it to return a ResponseEntity with 400 status if it\'s what the

2条回答
  •  春和景丽
    2020-12-18 12:14

    By the way, solution I gave before works, but my initial intention is bad idea: an error is an error and should not be handled on nominal flow. Throwing an exception, like Feign does, and handling it with an @ExceptionHandler is a better way to go in Spring MVC world.

    So two solutions:

    • add an @ExceptionHandler for FeignException
    • configure the FeignClient with an ErrorDecoder to translate the error in an Exception your business layer knows about (and already provide @ExceptionHandler for)

    I prefer second solution because received error message structure is likely to change from a client to an other, so you can extract finer grained data from those error with a per-client error decoding.

    FeignClient with conf (sorry for the noise introduced by feign-form)

    @FeignClient(value = "uaa", configuration = OauthFeignClient.Config.class)
    public interface OauthFeignClient {
    
        @RequestMapping(
                value = "/oauth/token",
                method = RequestMethod.POST,
                consumes = MULTIPART_FORM_DATA_VALUE,
                produces = APPLICATION_JSON_VALUE)
        DefaultOAuth2AccessToken token(Map formParams);
    
        @Configuration
        class Config {
    
            @Value("${oauth.client.password}")
            String oauthClientPassword;
    
            @Autowired
            private ObjectFactory messageConverters;
    
            @Bean
            public Encoder feignFormEncoder() {
                return new SpringFormEncoder(new SpringEncoder(messageConverters));
            }
    
            @Bean
            public Decoder springDecoder() {
                return new ResponseEntityDecoder(new SpringDecoder(messageConverters));
            }
    
            @Bean
            public Contract feignContract() {
                return new SpringMvcContract();
            }
    
            @Bean
            public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
                return new BasicAuthRequestInterceptor("web-client", oauthClientPassword);
            }
    
            @Bean
            public ErrorDecoder uaaErrorDecoder(Decoder decoder) {
                return (methodKey, response) -> {
                    try {
                        OAuth2Exception uaaException = (OAuth2Exception) decoder.decode(response, OAuth2Exception.class);
                        return new SroException(
                                uaaException.getHttpErrorCode(),
                                uaaException.getOAuth2ErrorCode(),
                                Arrays.asList(uaaException.getSummary()));
    
                    } catch (Exception e) {
                        return new SroException(
                                response.status(),
                                "Authorization server responded with " + response.status() + " but failed to parse error payload",
                                Arrays.asList(e.getMessage()));
                    }
                };
            }
        }
    }
    

    Common business exception

    public class SroException extends RuntimeException implements Serializable {
        public final int status;
    
        public final List errors;
    
        public SroException(final int status, final String message, final Collection errors) {
            super(message);
            this.status = status;
            this.errors = Collections.unmodifiableList(new ArrayList<>(errors));
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof SroException)) return false;
            SroException sroException = (SroException) o;
            return status == sroException.status &&
                    Objects.equals(super.getMessage(), sroException.getMessage()) &&
                    Objects.equals(errors, sroException.errors);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(status, super.getMessage(), errors);
        }
    }
    

    Error handler (extracted from a ResponseEntityExceptionHandler extension)

    @ExceptionHandler({SroException.class})
    public ResponseEntity handleSroException(SroException ex) {
        return new SroError(ex).toResponse();
    }
    
    
    

    Error response DTO

    @XmlRootElement
    public class SroError implements Serializable {
        public final int status;
    
        public final String message;
    
        public final List errors;
    
        public SroError(final int status, final String message, final Collection errors) {
            this.status = status;
            this.message = message;
            this.errors = Collections.unmodifiableList(new ArrayList<>(errors));
        }
    
        public SroError(final SroException e) {
            this.status = e.status;
            this.message = e.getMessage();
            this.errors = Collections.unmodifiableList(e.errors);
        }
    
        protected SroError() {
            this.status = -1;
            this.message = null;
            this.errors = null;
        }
    
        public ResponseEntity toResponse() {
            return new ResponseEntity(this, HttpStatus.valueOf(this.status));
        }
    
        public ResponseEntity toResponse(HttpHeaders headers) {
            return new ResponseEntity(this, headers, HttpStatus.valueOf(this.status));
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof SroError)) return false;
            SroError sroException = (SroError) o;
            return status == sroException.status &&
                    Objects.equals(message, sroException.message) &&
                    Objects.equals(errors, sroException.errors);
        }
    
        @Override
        public int hashCode() {
    
            return Objects.hash(status, message, errors);
        }
    }
    
    
    

    Feign client usage notice how errors are transparently handled (no try / catch) thanks to @ControllerAdvice & @ExceptionHandler({SroException.class})

    @RestController
    @RequestMapping("/uaa")
    public class AuthenticationController {
        private static final BearerToken REVOCATION_TOKEN = new BearerToken("", 0L);
    
        private final OauthFeignClient oauthFeignClient;
    
        private final int refreshTokenValidity;
    
        @Autowired
        public AuthenticationController(
                OauthFeignClient oauthFeignClient,
                @Value("${oauth.ttl.refresh-token}") int refreshTokenValidity) {
            this.oauthFeignClient = oauthFeignClient;
            this.refreshTokenValidity = refreshTokenValidity;
        }
    
        @PostMapping("/login")
        public ResponseEntity getTokens(@RequestBody @Valid LoginRequest userCredentials) {
            Map formData = new HashMap<>();
            formData.put("grant_type", "password");
            formData.put("client_id", "web-client");
            formData.put("username", userCredentials.username);
            formData.put("password", userCredentials.password);
            formData.put("scope", "openid");
    
            DefaultOAuth2AccessToken response = oauthFeignClient.token(formData);
            return ResponseEntity.ok(new LoginTokenPair(
                    new BearerToken(response.getValue(), response.getExpiresIn()),
                    new BearerToken(response.getRefreshToken().getValue(), refreshTokenValidity)));
        }
    
        @PostMapping("/logout")
        public ResponseEntity revokeTokens() {
            return ResponseEntity
                    .ok(new LoginTokenPair(REVOCATION_TOKEN, REVOCATION_TOKEN));
        }
    
        @PostMapping("/refresh")
        public ResponseEntity refreshToken(@RequestHeader("refresh_token") String refresh_token) {
            Map formData = new HashMap<>();
            formData.put("grant_type", "refresh_token");
            formData.put("client_id", "web-client");
            formData.put("refresh_token", refresh_token);
            formData.put("scope", "openid");
    
            DefaultOAuth2AccessToken response = oauthFeignClient.token(formData);
            return ResponseEntity.ok(new BearerToken(response.getValue(), response.getExpiresIn()));
        }
    }
    

    提交回复
    热议问题