2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

gRPCにおけるエラーハンドリング(Kotlin版)

Last updated at Posted at 2020-12-31

gRPCサーバーでエラーとなった場合、どのようにハンドリングすれば良いのか調べましたので、まとめておこうと思います。なお、Unary RPCs(SimpleRPC)の場合についてのみ記載します。Streamingを利用する場合については考慮しません。

ポイントは、Googleが公式にオススメしているメッセージ構造を利用することです。

具体的には、gRPCの公式サイトでオススメされているerror_details.protoのメッセージ構造を利用しましょう。このprotoファイルはproto-google-common-protos-{version}.jarに同梱されています。このprotoファイルには様々なメッセージタイプが定義されており、それらに対応するJavaクラスも同じJARに同梱されています。ですので、クライアント側/サーバー側の双方のアプリケーションコードで、これらのメッセージタイプを利用することができます。

独自のメッセージ構造を定義することはもちろんできますが、Googleがこれまでの経験をもとに考え抜いてくれたものを使えば、「車輪を再発明するムダ」を回避できます。使わない手はありません。(実際にはGoogleの考えてくれたメッセージ構造を参考に、PJ独自のエラー構造を設けた方が何かと便利だとは思いますが。)

今回ご紹介するソースコードの全量は以下にあります。
https://github.com/nyandora/grpc-kotlin-trial

サーバー側

private class HelloWorldService : GreeterGrpcKt.GreeterCoroutineImplBase() {
  override suspend fun sayHello(request: HelloRequest): HelloReply {
    // バリデーションを実行します。
    validate(request)

    return HelloReply.newBuilder().setMessage(request.name).build()
  }

  private fun validate(request: HelloRequest) {
    // nameプロパティのドメインは「10文字未満の文字列」とします。
    if (request.name.length >= 10) {
      val nameFieldError = BadRequest.FieldViolation.newBuilder()
            .setField("name")
            .setDescription("More than 10 characters are not allowed.").build()

      val badRequestError = BadRequest.newBuilder()
            .addFieldViolations(nameFieldError).build()

      val errorDetail = Metadata()
      errorDetail.put(ProtoUtils.keyForProto(badRequestError), badRequestError)

      throw StatusException(Status.INVALID_ARGUMENT, errorDetail)
    }
  }
}

ポイント

アプリケーションコードでio.grpc.StatusExceptionをスローすると、gRPCのKotlinコードジェネレータにより生成されたスタブコードがそれをハンドリングし、クライアントに適切に応答してくれます。ここで想定するコードジェネレータは、gRPC公式サイトのチュートリアルで利用されているものです。具体的には、こちらのgithubリポジトリにある「protoc-gen-grpc-kotlin」をKotlinコードジェネレータとして使っています。

StatusExceptionにはgoogleが定義しているgRPCステータスコードを設定します。上記の例のように入力値に問題がある場合は、Status.INVALID_ARGUMENTを使いましょう。

StatusExceptionにはエラーの詳細情報も詰めます。以下の目的を達成できる、必要最小限の情報だけを設定しましょう。

  • クライアント側の開発者が適切な対応をとれるようにする。
  • クライアント側のコードが適切にハンドリングできるようにする。

上記の例のように入力値に問題がある場合は、com.google.rpc.BadRequestを使いましょう。error_details.protoを見ると分かるのですが、このBadRequestにはネストされたメッセージタイプとしてFieldViolationが定義されています。FieldViolationにはフィールドごとに発生したエラー情報を詰めることができます。BadRequestには複数のFieldViolationを設定できるので、入力値の全量をチェックし、発生した全てのエラーを一気にクライアントに返す、ということもできます。

StatusExceptionにエラーの詳細情報を詰める場合は、io.grpc.Metadataを利用します。Metadataオブジェクトにエラーの詳細情報を設定し、それをStatusExceptionに詰めます。

クライアント側

class HelloWorldClient(private val channel: ManagedChannel) : Closeable {
  private val stub: GreeterGrpcKt.GreeterCoroutineStub = GreeterGrpcKt.GreeterCoroutineStub(channel)

  suspend fun greet(name: String) {
    val request = HelloRequest.newBuilder().setName(name).build()
    try {
      val response = stub.sayHello(request)
      println("Received: ${response.message}")
    } catch (e: StatusException) {
      if (e.status == Status.INVALID_ARGUMENT) {
        val badReqErrDetail = e.trailers[ProtoUtils.keyForProto(BadRequest.getDefaultInstance())]
        badReqErrDetail?.fieldViolationsList?.forEach {
          println("error occurred at ${it.field}")
          println("error description: ${it.description}")
        }
      }
    }
  }

  // 略
}

ポイント

gRPCコールの結果、エラー応答がかえってきた場合、クライアントコードではStatusExceptionがスローされます。クライアントではステータスコードを判定し、StatusExceptionからエラーの詳細情報を取り出すことができます。

バリデーションエラー以外のケース

上記の例では、バリデーションエラーが発生した場合にcom.google.rpc.BadRequestを利用しています。しかし発生するエラーはバリデーションエラーばかりではありません。エラーの内容に応じて適切なメッセージ構造を使いましょう。error_details.protoには、エラー時に利用できる様々なメッセージ構造が定義されています。適宜参照して使い分けをしましょう。

参考

GoogleのAPI設計ガイド: https://cloud.google.com/apis/design/errors

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?