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