GeekFactory

int128.hatenablog.com

Feignでレスポンスボディによる例外処理を行う

REST APIクライアントのSpring Cloud Feignを使う場合に異常系のステータスコードをハンドリングする方法を調べたのでメモです。

デフォルトの振る舞い

APIが400番台や500番台のステータスコードを返した場合、FeignはFeignExceptionをスローします。これは下記の記事に書いた通りです。

int128.hatenablog.com

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

APIゲートウェイとして振る舞う場合に有用と思います。

Feignのエラーハンドリングに関する記事がとても少ないので書いてみました。ご参考まで。