Feign ErrorDecoder :检索原始消息

Posted

技术标签:

【中文标题】Feign ErrorDecoder :检索原始消息【英文标题】:Feign ErrorDecoder : retrieve the original message 【发布时间】:2019-04-30 16:51:11 【问题描述】:

我使用 ErrorDecoder 返回正确的异常而不是 500 状态代码。

有没有办法在解码器中检索原始消息。我可以看到它在 FeignException 内部,但不在 decode 方法中。我只有“状态代码”和一个空的“原因”。

public class CustomErrorDecoder implements ErrorDecoder 

    private final ErrorDecoder errorDecoder = new Default();

    @Override
    public Exception decode(String s, Response response) 

        switch (response.status()) 

            case 404:
                return new FileNotFoundException("File no found");
            case 403:
                return new ForbiddenAccessException("Forbidden access");
        

        return errorDecoder.decode(s, response);
    

这里是原始消息:“消息”:“禁止访问文件”

feign.FeignException: status 403 reading ProxyMicroserviceFiles#getUserRoot(); content:
"timestamp":"2018-11-28T17:34:05.235+0000","status":403,"error":"Forbidden","message":"Access to the file forbidden","path":"/root"

我还使用我的 FeignClient 接口,如 RestController,因此我不使用任何其他填充了可以封装方法调用的代理的控制器。

   @RestController
   @FeignClient(name = "zuul-server")
   @RibbonClient(name = "microservice-files")

   public interface ProxyMicroserviceFiles 

                @GetMapping(value = "microservice-files/root")
                Object getUserRoot();

                @GetMapping(value = "microservice-files/file/id")
                Object getFileById(@PathVariable("id") int id);

    

【问题讨论】:

【参考方案1】:

这里有一个解决方案,消息实际上是在响应体中作为一个流。

package com.clientui.exceptions;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.*;

import java.io.*;

public class CustomErrorDecoder implements ErrorDecoder 

    private final ErrorDecoder errorDecoder = new Default();

    @Override
    public Exception decode(String s, Response response) 

        String message = null;
        Reader reader = null;

        try 
            reader = response.body().asReader();
            //Easy way to read the stream and get a String object
            String result = CharStreams.toString(reader);
            //use a Jackson ObjectMapper to convert the Json String into a 
            //Pojo
            ObjectMapper mapper = new ObjectMapper();
            //just in case you missed an attribute in the Pojo     
          mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
            //init the Pojo
            ExceptionMessage exceptionMessage = mapper.readValue(result, 
                                                ExceptionMessage.class);

            message = exceptionMessage.message;

         catch (IOException e) 

            e.printStackTrace();
        finally 

            //It is the responsibility of the caller to close the stream.
            try 

                if (reader != null)
                    reader.close();

             catch (IOException e) 
                e.printStackTrace();
            
        

        switch (response.status()) 

            case 404:
                return new FileNotFoundException(message == null ? "File no found" : 
                                                                     message);
            case 403:
                return new ForbiddenAccessException(message == null ? "Forbidden 
                                                              access" : message);

        

        return errorDecoder.decode(s, response);
    

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    public static class ExceptionMessage

        private String timestamp;
        private int status;
        private String error;
        private String message;
        private String path;

    

【讨论】:

我也有类似的问题。这真的是找回尸体的唯一方法吗?似乎有相当多的样板开销“只是”从响应中读取正文作为字符串? @msilb 您可以直接以 json 格式返回结果字符串并在最终客户端中反序列化它 @kaizokum 我也遇到了同样的问题。但是,当我执行 String result = CharStreams.toString(reader);我收到流已经关闭的异常。 @nsivaram90 你找到解决方案了吗?我遇到了同样的错误。 对于任何寻找@kaizokum 问题答案的人:***.com/questions/61472139/…【参考方案2】:

如果你和我一样,真的只是想从失败的 Feign 调用中获得内容,而无需所有这些自定义解码器和样板,那么有一种很老套的方法来做到这一点。

如果我们在创建 FeignException 并且存在响应主体时查看它,它会像这样组装异常消息:

if (response.body() != null) 
    String body = Util.toString(response.body().asReader());
    message += "; content:\n" + body;

因此,如果您在响应正文之后,您可以通过解析异常消息将其拉出,因为它由换行符分隔。

String[] feignExceptionMessageParts = e.getMessage().split("\n");
String responseContent = feignExceptionMessageParts[1];

如果你想要这个对象,你可以使用 Jackson 之类的东西:

MyResponseBodyPojo errorBody = objectMapper.readValue(responseContent, MyResponseBodyPojo.class);

我并不认为这是一种聪明的方法或最佳实践。

【讨论】:

原始响应内容也可以(很可能)包含换行符,因此我们要查找的部分不是feignExceptionMessageParts[1],而是整个数组没有第零个元素【参考方案3】:

如果你想得到response payload body,除了Feign异常,使用这个方法即可:

feignException.contentUTF8();

例子:

    try 
        itemResponse = call(); //method with the feign call
     catch (FeignException e) 
        logger.error("ResponseBody: " + e.contentUTF8());
    

【讨论】:

这不适用于旧版本的 Spring Cloud 这就是我要找的。确切的解决方案... 它返回一个 JSON 作为字符串。如何将其转换回 Model 对象? @romeucr 如果你有一个代表这个 JSON 的类,你可以使用 jackson 来解析它。 是的!经过一番搜索,我意识到该怎么做。超级简单:String string = ex.contentUTF8(); MyClass myClass = new ObjectMapper().readValue(string, MyClass.class);【参考方案4】:

原始消息在响应正文中,正如已经回答的那样。但是,我们可以使用 Java 8 Streams 来减少样板的数量:

public class CustomErrorDecoder implements ErrorDecoder 

  private final ErrorDecoder errorDecoder = new Default();

  @Override
  public Exception decode(String s, Response response) 
    String body = "4xx client error";
    try 
        body = new BufferedReader(response.body().asReader(StandardCharsets.UTF_8))
          .lines()
          .collect(Collectors.joining("\n"));
     catch (IOException ignore) 

    switch (response.status()) 

        case 404:
            return new FileNotFoundException(body);
        case 403:
            return new ForbiddenAccessException(body);
    

    return errorDecoder.decode(s, response);
  

【讨论】:

【参考方案5】:

建议使用输入流代替阅读器并将其映射到您的对象。

package com.clientui.exceptions;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.*;

import java.io.*;

public class CustomErrorDecoder implements ErrorDecoder 

    private final ErrorDecoder errorDecoder = new Default();

    @Override
    public Exception decode(String s, Response response) 

        String message = null;
        InputStream responseBodyIs = null;
        try 
            responseBodyIs = response.body().asInputStream();
            ObjectMapper mapper = new ObjectMapper();
            ExceptionMessage exceptionMessage = mapper.readValue(responseBodyIs, ExceptionMessage.class);

            message = exceptionMessage.message;

         catch (IOException e) 

            e.printStackTrace();
            // you could also return an exception
            return new errorMessageFormatException(e.getMessage());
        finally 

            //It is the responsibility of the caller to close the stream.
            try 
                if (responseBodyIs != null)
                    responseBodyIs.close();
             catch (IOException e) 
                e.printStackTrace();
            
        

        switch (response.status()) 

            case 404:
                return new FileNotFoundException(message == null ? "File no found" :
                        message);
            case 403:
                return new ForbiddenAccessException(message == null ? "Forbidden access" : message);

        

        return errorDecoder.decode(s, response);
    

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    public static class ExceptionMessage

        private String timestamp;
        private int status;
        private String error;
        private String message;
        private String path;

    

【讨论】:

【参考方案6】:

接受答案的一些重构和代码风格:

@Override
@SneakyThrows
public Exception decode(String methodKey, Response response) 
  String message;

  try (Reader reader = response.body().asReader()) 
    String result = StringUtils.toString(reader);
    message = mapper.readValue(result, ErrorResponse.class).getMessage();
  

  if (response.status() == 401) 
    return new UnauthorizedException(message == null ? response.reason() : message);
  
  if (response.status() == 403) 
    return new ForbiddenException(message == null ? response.reason() : message);
  
  return defaultErrorDecoder.decode(methodKey, response);

【讨论】:

以上是关于Feign ErrorDecoder :检索原始消息的主要内容,如果未能解决你的问题,请参考以下文章

Netflix Feign:404 上 ErrorDecoder 中的响应正文为空

自定义拦截feign错误

Feign实现自定义错误处理

如何微调 Spring Cloud Feign 客户端?

Spring Cloud 异常处理

SpringCloud-feign客户端统一处理下游服务自定义异常(1.5.x版本下可以)