22

I use a ErrorDecoder to return the right exception rather than a 500 status code.

Is there a way to retrieve the original message inside the decoder. I can see that it is inside the FeignException, but not in the decode method. All I have is the 'status code' and a empty '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);
    }
}

Here the original message : "message":"Access to the file forbidden"

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"}

Also I use my FeignClient interface like a RestController so I don't use any other Controler populated with the proxy that could encapsulate the methods calls.

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

    }
kaizokun
  • 926
  • 3
  • 9
  • 31

7 Answers7

23

If you want to get the response payload body, with the Feign exception, just use this method:

feignException.contentUTF8();

Example:

    try {
        itemResponse = call(); //method with the feign call
    } catch (FeignException e) {
        logger.error("ResponseBody: " + e.contentUTF8());
    }
Eduardo Briguenti Vieira
  • 4,351
  • 3
  • 37
  • 49
22

Here is a solution, the message is actually in the response body as a stream.

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;

    }
}
kaizokun
  • 926
  • 3
  • 9
  • 31
  • 1
    I have a similar issue. Is this really the only way to retrieve the body? Seems like quite a bit of boilerplate overhead to "just" read the body from the response as string? – msilb Dec 11 '19 at 12:01
  • @msilb you could return the result String directly at the json format and deserialize it in your final client – kaizokun Dec 12 '19 at 14:43
  • 3
    @kaizokum I have also ran into the same problem . However when I do String result = CharStreams.toString(reader); I'm getting stream already closed exception. – nsivaram90 Feb 04 '20 at 12:07
  • @nsivaram90 did you find the solution? I am getting same error. – ssbh Dec 15 '20 at 01:04
  • For anyone looking for an answer to @kaizokum's problem: https://stackoverflow.com/questions/61472139/openfeign-errordecoder-caused-java-io-ioexception-stream-is-closed – Nora Na Jan 31 '22 at 12:39
3

It is suggested to use input stream instead of reader and map it to your object.

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;

    }
}
Rostamian
  • 41
  • 2
2

Some refactoring and code style on the accepted answer:

@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);
}
Zon
  • 18,610
  • 7
  • 91
  • 99
1

If you're like me and really just want the content out of a failed Feign call without all these custom decoders and boilerplate, there is a hacky way do this.

If we look at FeignException when it is being created and a response body exists, it assembles the exception message like so:

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

Therefore if you're after the response body, you can just pull it out by parsing the Exception message since it is delimited by a newline.

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

And if you want the object, you can use something like Jackson:

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

I do not claim this is a smart approach or a best practice.

Steve
  • 538
  • 4
  • 17
  • 1
    Original response content can (most likely will) contain newlines as well, so the part we're looking for not `feignExceptionMessageParts[1]` but entire array without the zeroth element – Deltharis Mar 18 '21 at 13:05
0

The original message is within the Response body, as already answered. However, we can reduce the amount of boilerplate using Java 8 Streams to read it:

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);
  }
}
dbaltor
  • 2,737
  • 3
  • 24
  • 36
0

On 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
)
  • Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. **Would you kindly [edit] your answer to include additional details for the benefit of the community?** – Jeremy Caney Jun 06 '23 at 00:29