服务端返回401时Feign返回FeignException而非ResponseEntity的问题
我们的应用使用spring-cloud-starter-openfeign:4.1.0实现服务间通信,客户端代码如下:
@FeignClient(name = "authClient", url = "${spring.microservice.tenant-auth.host}") public interface AuthClient { @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) ResponseEntity<AuthenticationResponse> getAuthToken(AuthFormData formData); } public class AuthFormData { private String grant_type; private String client_id; private String client_secret; public void setGrantType(String grantType) { this.grant_type = grantType; } public void setClientId(String clientId) { this.client_id = clientId; } public void setClientSecret(String clientSecret) { this.client_secret = clientSecret; } } public class AuthenticationResponse { @JsonProperty("access_token") private String accessToken; @JsonProperty("token_type") private String tokenType; @JsonProperty("expires_in") private int expiresIn; @JsonProperty("scope") private String scope; @JsonProperty("refresh_token") private String refreshToken; @JsonProperty("id_token") private String idToken; }
代码运行正常,但存在一个问题:当服务端返回401时,调用方会抛出FeignException,调用代码如下:
AuthenticationResponse getToken(AuthFormData authFormData) { var authResponse = authClient.getAuthToken(authFormData); var body = authResponse.getBody(); if (!authResponse.getStatusCode().is2xxSuccessful() || body == null) { throw new IllegalStateException("Failed to get auth token " + authResponse); } return body; }
对应的堆栈信息:
feign.FeignException$Unauthorized: [401 Unauthorized] during [POST] to [https://somethin.com/oauth/token] [AuthClient#getAuthToken(AuthFormData)]: [] at feign.FeignException.clientErrorStatus(FeignException.java:224) ~[feign-core-13.1.jar:na] at feign.FeignException.errorStatus(FeignException.java:203) ~[feign-core-13.1.jar:na] at feign.FeignException.errorStatus(FeignException.java:194) ~[feign-core-13.1.jar:na] at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:103) ~[feign-core-13.1.jar:na] at feign.InvocationContext.decodeError(InvocationContext.java:126) ~[feign-core-13.1.jar:na] at feign.InvocationContext.proceed(InvocationContext.java:72) ~[feign-core-13.1.jar:na] at feign.ResponseHandler.handleResponse(ResponseHandler.java:63) ~[feign-core-13.1.jar:na] at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:114) ~[feign-core-13.1.jar:na] at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:70) ~[feign-core-13.1.jar:na] at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:99) ~[feign-core-13.1.jar:na] at jdk.proxy2/jdk.proxy2.$Proxy113.getAuthToken(Unknown Source) ~[na:na] at c.c.o.p.m.AuthenticationTokenProvider.getToken(AuthenticationTokenProvider.java:34) ~[classes/:0.0.1-SNAPSHOT]
奇怪的是,服务端返回200或500时,能正常返回对应状态码的ResponseEntity。请问为何401会抛出异常,如何让AuthClient返回带401状态的ResponseEntity?
Feign的默认ErrorDecoder会将**4xx客户端错误(除404外)**转换成FeignException抛出,而5xx服务端错误则会被直接包装进ResponseEntity返回。这就是401会触发异常,而500能正常返回ResponseEntity的核心原因。
要让Feign返回401状态的ResponseEntity,需要自定义Feign的错误处理逻辑,跳过对401的异常转换。
方法一:自定义ErrorDecoder放行401状态
创建自定义ErrorDecoder实现,对401状态不抛出异常,交给后续代码处理:
import feign.Response; import feign.codec.ErrorDecoder; import org.springframework.cloud.openfeign.FeignClientProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignConfig { @Bean public ErrorDecoder errorDecoder(FeignClientProperties properties) { return new ErrorDecoder.Default() { @Override public Exception decode(String methodKey, Response response) { // 对401状态不抛出异常,直接返回null让Feign继续处理成ResponseEntity if (response.status() == 401) { return null; } return super.decode(methodKey, response); } }; } }
在@FeignClient注解中指定该配置类:
@FeignClient(name = "authClient", url = "${spring.microservice.tenant-auth.host}", configuration = FeignConfig.class) public interface AuthClient { // 原有方法保持不变 }
方法二:配置统一Decoder处理所有状态码
通过自定义SpringDecoder,让Feign将所有状态码的响应都包装成ResponseEntity返回,包括4xx:
import org.springframework.cloud.openfeign.support.SpringDecoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import java.util.List; @Configuration public class FeignConfig { @Bean public SpringDecoder springDecoder() { List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter()); return new SpringDecoder(() -> converters); } }
这种方式下,所有状态码的响应都会被封装为ResponseEntity,不会触发Feign异常。
验证效果
修改完成后,调用authClient.getAuthToken(authFormData)时,服务端返回401的情况下,会得到一个状态码为401的ResponseEntity,可以在调用方法中正常判断状态码并进行后续处理。
内容的提问来源于stack exchange,提问作者Sergey Tsypanov

