Feignでレスポンスボディによる例外処理を行う
REST APIクライアントのSpring Cloud Feignを使う場合に異常系のステータスコードをハンドリングする方法を調べたのでメモです。
デフォルトの振る舞い
APIが400番台や500番台のステータスコードを返した場合、FeignはFeignException
をスローします。これは下記の記事に書いた通りです。
FeignException
にはステータスコードが含まれるので、例外オブジェクトのstatus
プロパティで例外処理ができます。しかし、レスポンスボディで例外処理するのはこの仕組みではできません。
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/FeignException.java
public class FeignException extends RuntimeException { private static final long serialVersionUID = 0; private int status; //(中略) public static FeignException errorStatus(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); try { if (response.body() != null) { String body = Util.toString(response.body().asReader()); message += "; content:\n" + body; } } catch (IOException ignored) { // NOPMD } return new FeignException(response.status(), message); } }
レスポンスボディによる例外処理
レスポンスボディの内容を元に例外処理を行いたい場合を想定します。例えば、バリデーションエラーの時にステータスコード400でエラー理由がレスポンスボディに入るといった場合です。
このような場合、既存のFeignException
ではレスポンスボディを読み取れないので、ErrorDecoder
を使う必要があります。ErrorDecoder
を使うと、APIが400番台や500番台を返した場合にどのような例外を発生させるか設定できます。詳しくは Custom error handling · OpenFeign/feign Wiki · GitHub に説明があります。
ここでは、ErrorDecoder
でレスポンスボディをPOJOに変換してみましょう。Spring Cloud Feignに含まれるSpringDecoder
を使うと、Springで用意されているConvertでレスポンスボディをオブジェクトに変換できます。
import feign.Response import feign.codec.Decoder import feign.codec.ErrorDecoder class ErrorResponseDecoder implements ErrorDecoder { // Spring Cloud FeignのSpringDecoderが注入される @Inject Decoder decoder @Override Exception decode(String methodKey, Response response) { def errorResponse = decoder.decode(response, ErrorResponse) as ErrorResponse new ErrorResponseException(response.status(), errorResponse) } } // レスポンスボディの型 @Immutable class ErrorResponse { int code String message } // レスポンスボディを含む例外クラス class ErrorResponseException extends FeignException { final ErrorResponse errorResponse def ErrorResponseException(int status, ErrorResponse errorResponse1) { super(status, errorResponse1.toString()) errorResponse = errorResponse1 } }
ErrorDecoder
を有効にするにはFeignのConfigurationを使います。
@Configuration class HelloClientConfiguration { @Bean ErrorDecoder errorDecoder() { new ErrorResponseDecoder() } } @FeignClient(name = 'hello', url = '${hello.service.url}', configuration = HelloClientConfiguration) interface HelloClient { //... }
上記の仕組みを使うと、ErrorResponseException
をキャッチすることでレスポンスボディによる例外処理ができます。
レスポンスボディのパススルー
外部APIのエラーレスポンスをそのままクライアントに返したい場合は、@ControllerAdvice
で共通処理を入れるとよいでしょう。
@Slf4j @ControllerAdvice class ResponseStatusPropagation { @ExceptionHandler(ErrorResponseException) ResponseEntity handleErrorResponseException(ErrorResponseException e) { log.error("Handled error response from API", e) def cause = e.cause as ErrorResponseException ResponseEntity .status(cause.status()) .contentType(MediaType.APPLICATION_JSON) .body(cause.errorResponse) } }
Feignのエラーハンドリングに関する記事がとても少ないので書いてみました。ご参考まで。