OpenFeign-エラーコード分岐処理(ErrorDecoder)


前回の記事に続き,今回は通常の応答ではなく復号エラー応答のErrorDecoderを実現することを試みる.
まずFeignで応答エラーを処理する方法について理解する.

ErrorDecoder


Springcloud openfeignは、ErrorDecoderを介して応答値が200でない場合にエラーを投げ出す.
Feignのdefault ErrorDecoderはResponseEntityErrorDecoderであり、対応するErrorDecoderはFeignExceptionである.

FeignClientExceptionとFeignServer Exception

  • ステータスコード400番→FeignClientException
  • ステータスコード500番→FeignServerException
  • FeignClientException


    FeignExceptionを継承するFeignClientExceptionは、ステータスコードごとに異なるサブクラスを持つ.
    400ならFeignExceptionBadRequest()、401はFeignExceptionです.Unautorized()認識で、次のコードでどのような異常があるかを確認できます.
    // feign.FeignException#clientErrorStatus
    
    switch (status) {
      case 400:
        return new BadRequest(message, request, body);
      case 401:
        return new Unauthorized(message, request, body);
      case 403:
        return new Forbidden(message, request, body);
      case 404:
        return new NotFound(message, request, body);
      case 405:
        return new MethodNotAllowed(message, request, body);
      case 406:
        return new NotAcceptable(message, request, body);
      case 409:
        return new Conflict(message, request, body);
      case 410:
        return new Gone(message, request, body);
      case 415:
        return new UnsupportedMediaType(message, request, body);
      case 429:
        return new TooManyRequests(message, request, body);
      case 422:
        return new UnprocessableEntity(message, request, body);
      default:
        return new FeignClientException(status, message, request, body);
    }

    FeignServerException


    500回のレスポンスを処理するFeignServerExceptionのタイプは多くありません.
    // feign.FeignException#serverErrorStatus
    switch (status) {
      case 500:
        return new InternalServerError(message, request, body);
      case 501:
        return new NotImplemented(message, request, body);
      case 502:
        return new BadGateway(message, request, body);
      case 503:
        return new ServiceUnavailable(message, request, body);
      case 504:
        return new GatewayTimeout(message, request, body);
      default:
        return new FeignServerException(status, message, request, body);
    }

    エラーコード処理


    外部サービスが特定のエラーコードを有する場合に応答する場合、異なる動作が必要になる場合があります.この場合,エラー応答を割り当てるbodyが必要であり,FallbackFactoryではその操作を実行するたびに面倒なことになる.
    このとき、ErrorDecoderは、基本的なエラー応答パケットを処理することができる.

    間違った方向をきれいにしてもらえませんか?


    以下の応答がある場合は、エラーを吐かずに空のlistで応答することを考慮してください.
    前シリーズのBeerClientに基づきます.
    status code 400
    {
        "result": "FAILED",
        "data": null,
        "error": {
            "code": "FOO_ERROR",
            "message": "error message",
            "data": null
        }
    }
    この場合、元のFeignExceptionであれば.BadRequestは投げ出されますが、FeignExceptionの応答bodyはbytearrayなので、応答を分解するたびに面倒です.
    以下の方法で改善します.
  • 応答は、特定のフォーマットで提供される:
  • エラーコード
  • が存在する場合、
  • どちらも満足していればBeerClientHandledExceptionを投げます.

    テスト


    上記の要件をテストで表現します.
    @Test
    fun handledError() {
        val responseBody = """
                {
                    "result": "FAILED",
                    "data": null,
                    "error": {
                        "code": "FOO_ERROR",
                        "message": "error message",
                        "data": null
                    }
                }
            """.trimIndent()
    
        mockServer.stubFor(
            get(urlPathEqualTo("/beers"))
                .withQueryParam("size", equalTo("2"))
                .withHeader("Authorization", equalTo("AUTH-KEY"))
                .withHeader("Content-Type", equalTo("application/json"))
                .willReturn(
                    aResponse()
                        .withStatus(HttpStatus.BAD_REQUEST.value())
                        .withBody(responseBody)
                        .withHeader("Content-Type", "application/json")
                )
        )
    
    		val then = shouldThrow<BeerClientHandledException> { sut.getBeer(2) }
    
        then.errorCode shouldBe "FOO_ERROR"
        then.message shouldBe "error message"
    }
    実行してみると、結果はもちろん失敗した.

    ErrorDecoder実装


    以下のBerClientErrorDecoderによりErrorDecoderを実現する.
    class BeerClientErrorDecoder: ErrorDecoder {
      override fun decode(methodKey: String, response: Response): Exception {
          val errorData = parse(response)
          errorData?.code?.let {
              return BeerClientHandledException(it, errorData.message)
          }
          return ErrorDecoder.Default().decode(methodKey, response)
      }
    
      private fun parse(response: Response): CommonResponse.ErrorData? {
          return runCatching {
               objectMapper.readValue(response.body().asInputStream(), CommonResponse::class.java)?.error
          }.getOrNull()
      }
    
      private val objectMapper = jacksonObjectMapper()
          .findAndRegisterModules()
    }
    
    data class CommonResponse<T>(
        val result: String?,
        val data: T?,
        val error: ErrorData?
    ) {
        data class ErrorData(
            val code: String?,
            val message: String?,
            val data: Map<String, Any?>?
        )
    }
    
    その後、クライアントは、対応するErrorDecoderを受信するように設定される.
    class BeerClientConfiguration {
        @Bean
        fun errorDecoder(): ErrorDecoder {
            return BeerClientErrorDecoder()
        }
    }
    もう一度テストを実行すると、テストが合格したことがわかります.

    ソース


    上記の例は、次のソースコードで詳細に表示できます.
    https://github.com/hanqyu/feign-decoder-example