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のエラーハンドリングに関する記事がとても少ないので書いてみました。ご参考まで。

Jenkins PipelineでGitリポジトリにpushする

JenkinsのジョブでGitリポジトリにブランチやタグをpushしたい場合があります。Jenkinsfileでどのように実装するか調べてみました。

実装例

お急ぎの方は下記のコードを参考にしてください。

def userRemoteConfig = scm.userRemoteConfigs.head()
withCredentials([[$class: 'UsernamePasswordMultiBinding', 
  credentialsId: userRemoteConfig.credentialsId,
  usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASSWORD']]) {
  def url = userRemoteConfig.url.replace('://', "://${env.GIT_USER}:${env.GIT_PASSWORD}@")
  sh "git push $url --tags"
}

PipelineでSCMにGitが指定されている必要があります。また、上記は git push しか書いていませんが、実際にはユーザ名やメールアドレスを設定したり、 git add したりするコードも必要です。

どのように実現しているのか

Jenkins PipelineのJenkinsfileでは以下のコードをよく使います。

node {
  stage('Fetch') {
    checkout scm
  }
}

では、ここに出てくる scm 変数とは何者なんでしょうか。調べてみたらCloudbeeの記事に書いてありました。

support.cloudbees.com

SCMにGitを指定した場合、 scm 変数は GitSCM クラスインスタンスになります。GitSCM クラスには以下のメソッドが用意されています。

    public List<UserRemoteConfig> getUserRemoteConfigs() {...}

ここでいう UserRemoteConfig クラス こそがGitの接続情報になります。UserRemoteConfig クラスには以下のプロパティが定義されています。

  • name
  • refspec
  • url
  • credentialsId

ということは、GitSCM クラス→UserRemoteConfig クラス→credentialsIdプロパティ→withCredentialsの流れでGitの接続情報を取得できます。

ただし、Jenkins Pipelineの標準では scm 変数のプロパティに対するアクセスは禁止されています。このため、Jenkinsの設定から scm 変数のプロパティに対するアクセスを許可する必要があります。これは「Jenkinsの設定」で指定できます。

いつの日になるか分かりませんが、PipelineにGitアクセス機能が追加されるのが待ち遠しいですね。

Swagger YAMLのバリデーションをCIで回す

Swagger YAMLを書いていると間違いに気づかないことがよくあります。リポジトリにpushした時にJenkinsやCircle CIなどで構文チェックできるとミスを防げるます。そこで、GradleのプラグインにSwagger YAMLのバリデーションを追加してみました。

使い方

build.gradleに下記を書きます。

plugins {
  id 'org.hidetake.swagger.generator' version '1.6.0'
}

validateSwagger {
  inputFile = file('petstore.yaml')
}

下記のようにvalidateSwaggerタスクを実行すると、YAMLのバリデーションが実行されます。

% ./gradlew validateSwagger
:validateSwagger

YAMLに間違いがあると、下記のようなエラーを表示してくれます。

:validateSwagger FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':validateSwagger'.
> Invalid Swagger YAML: /Users/hidetake/repo/gradle-swagger-generator-plugin/acceptance-test/validator/petstore.yaml
  ---
  - level: "error"
    schema:
      loadingURI: "http://swagger.io/v2/schema.json#"
      pointer: "/definitions/parametersList/items"
    instance:
      pointer: "/paths/~1pets~1{petId}/get/parameters/0"
    domain: "validation"
    keyword: "oneOf"
    message: "instance failed to match exactly one schema (matched 0 out of 2)"
    matched: 0
    nrSchemas: 2
(以下略)

仕組み

Swaggerで公式に用意されているJSONスキーマYAMLを検証しています。

https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json

Gradleプラグインについて

Gradle Swagger Generator Pluginはバリデーションだけでなくコード生成やドキュメント生成の機能も提供しています。詳しい使い方はREADMEを参照してください。

github.com