Feign ErrorDecoder:获取原始消息

23

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

是否有一种方法可以在解码器中检索原始消息。我可以看到它在 FeignException 中,但不在 decode 方法中。我只有 '状态代码' 和一个空的 'reason'。

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

这里是原始信息: "message":"访问文件被拒绝"

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

    }
7个回答

25

如果您想获得Feign异常中的响应体负载,只需使用此方法:

如果您希望获取Feign异常中的响应负载正文,只需使用此方法:

feignException.contentUTF8();

例子:

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

这在旧版本的Spring Cloud上不起作用。 - Brendon Iwata
这就是我一直在寻找的。确切的解决方案... - santu
它返回一个JSON字符串。如何将其转换回模型对象? - romeucr
2
@romeucr 如果您有一个表示此 JSON 的类,可以使用 Jackson 解析它。 - Eduardo Briguenti Vieira
3
是的!经过一些搜索,我意识到如何做到这一点。它非常简单: String string = ex.contentUTF8(); MyClass myClass = new ObjectMapper().readValue(string, MyClass.class); - romeucr

22

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

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;

    }
}

1
我有类似的问题。这真的是唯一检索正文的方法吗?看起来需要相当多的样板代码开销才能“只是”将正文作为字符串从响应中读取? - msilb
@msilb,你可以直接以JSON格式返回结果字符串,并在最终客户端中进行反序列化。 - kaizokun
3
我也遇到了同样的问题。但是当我执行下面这行代码时: String result = CharStreams.toString(reader); 我收到了"stream already closed"异常。 - nsivaram90
@nsivaram90,你找到解决方案了吗?我遇到了同样的错误。 - ssbh
对于任何寻找@kaizokum问题答案的人:https://dev59.com/GLroa4cB1Zd3GeqPoKO7 - Nora Na

3
建议使用输入流而不是读取器,并将其映射到您的对象中。
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;

    }
}

2

对被接受的答案进行了一些重构和代码风格调整:

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

1
如果您和我一样,只是想从失败的Feign调用中获取内容而不需要所有这些自定义解码器和样板代码,那么有一种hacky的方法可以做到这一点。
当创建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);

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


1
原始响应内容可能包含换行符,因此我们要查找的部分不是 feignExceptionMessageParts[1],而是整个数组,不包括第零个元素。 - Deltharis

1

原始消息已经在响应体中,如已回答。但是,我们可以使用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);
  }
}

这种方法可以说比将答案转换为POJO更安全和多功能,因为它可以处理任何类型的异常对象(无缝地将其转换为字符串表示形式,就像在日志或Postman中看到的那样)。效果非常好。 在其他答案中,你必须确切地知道所有可能出现的异常的内容,否则它们将在ObjectMapper中失败。 - undefined

0
在Kotlin上:
@Component
class FeignExceptionHandler : ErrorDecoder {
    
    override fun decode(methodKey: String, response: Response): Exception {
        return ResponseStatusException(
            HttpStatus.valueOf(response.status()),
            readMessage(response).message
        )
    }

    private fun readMessage(response: Response): ExceptionMessage {
        return response.body().asInputStream().use {
                val mapper = ObjectMapper()
                mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                mapper.readValue(it, ExceptionMessage::class.java)
            }
    }
}

data class ExceptionMessage(
    val timestamp: String? = null,
    val status: Int = 0,
    val error: String? = null,
    val message: String? = null,
    val path: String? = null
)

感谢您为Stack Overflow社区做出的贡献。这可能是一个正确的答案,但如果您能提供代码的额外解释,让开发人员能够理解您的推理过程,那将非常有用。对于不太熟悉语法或难以理解概念的新开发人员来说,这尤其有帮助。您是否可以编辑您的答案,包含更多细节,以造福整个社区? - Jeremy Caney
感谢您为Stack Overflow社区做出的贡献。这可能是一个正确的答案,但如果您能提供代码的额外解释,那将对开发人员理解您的思路非常有帮助。对于那些对语法不太熟悉或者正在努力理解概念的新手开发人员来说,这尤其有用。您是否可以编辑您的答案,以便为社区的利益提供更多细节? - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接