VISITS Technologies Advent Calendar 2019 23日目は@istshが担当します。
この記事はサンプルコードを使って解説してきます。
エラーをカスタマイズしたい
おそらくgRPCのエラー実装において、
status.New(codes.Unauthenticated, "not authenticated").Err()
のように書いたことがあると思います。
しかし、これではnot authenticated
というエラーメッセージしか扱えません。
私がいるプロジェクトでは、下記の3点を返す必要がありました。
- エラーコード(StatusCodeではない)
- ロケール
- エラーメッセージ
この記事では、これらをgRPCで返し、さらにgRPC-Gateway(HTTP)で返すところまでの実装を紹介します。
LocalizedMessage
go get -u google.golang.org/genproto/googleapis/rpc/errdetails
// Provides a localized error message that is safe to return to the user
// which can be attached to an RPC error.
type LocalizedMessage struct {
// The locale used following the specification defined at
// http://www.rfc-editor.org/rfc/bcp/bcp47.txt.
// Examples are: "en-US", "fr-CH", "es-MX"
Locale string `protobuf:"bytes,1,opt,name=locale,proto3" json:"locale,omitempty"`
// The localized error message in the above locale.
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
error_details.pb.go#L670
このLocalizedMessage
を使うことで、ロケール
とエラーメッセージ
を扱えそうです。
WithDetails
status.New(codes.Unauthenticated, "not authenticated").Err()
で上記のLocalizedMessage
を扱うには、下記のようにWithDetails
というメソッドを使います。
status.New(status.New(codes.Unauthenticated, "not authenticated").WithDetails(
&errdetails.LocalizedMessage{
Locale: LocaleJaJp,
Message: "ユーザーの認証ができませんでした。",
},
))
ちなみにWithDetails
は、可変長引数を取ります。
func (s *Status) WithDetails(details ...proto.Message) (*Status, error) {
// 省略...
}
よって、下記のようにLocalizedMessage
を複数渡すことが可能です、
status.New(status.New(codes.Unauthenticated, "not authenticated").WithDetails(
&errdetails.LocalizedMessage{
Locale: LocaleJaJp,
Message: "ユーザーの認証ができませんでした。",
},
&errdetails.LocalizedMessage{
Locale: LocaleEnUs,
Message: "Unauthenticated",
},
))
これで、あるエラーにおいて日本語や英語のエラーメッセージを扱えることがわかりました。
エラーコード
エラーコードを扱うには、自前で用意する必要があるようです。
type ErrorCode struct {
ErrorCode string `protobuf:"bytes,1,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ErrorCode) Reset() { *m = ErrorCode{} }
func (m *ErrorCode) String() string { return proto.CompactTextString(m) }
func (*ErrorCode) ProtoMessage() {}
これはerror.proto
から生成したコードです。
WithDetails
は、下記のproto.Message
のインターフェースを実装した構造体であれば渡せるので、自動生成しなくても問題ありません。
// Message is implemented by generated protocol buffer messages.
type Message interface {
Reset()
String() string
ProtoMessage()
}
カスタムエラーの定義
ここまでのコードをまとめると、下記のようになります。
status.New(codes.Unauthenticated, "not authenticated").WithDetails(
&pbv1.ErrorCode{
ErrorCode: "USER_UNAUTHENTICATED",
},
&errdetails.LocalizedMessage{
Locale: LocaleJaJp,
Message: "ユーザーの認証ができませんでした。",
},
&errdetails.LocalizedMessage{
Locale: LocaleEnUs,
Message: "Unauthenticated",
},
)
カスタムエラーをgRPC-Gateway(HTTP)で返す
カスタムHTTPエラーの実装
func HTTPError(_ context.Context, _ *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, _ *http.Request, err error) {
s, ok := status.FromError(err)
if !ok {
s = status.New(codes.Unknown, err.Error())
}
// 省略...
ed := &pbv1.Error_ErrorDetail{}
if len(s.Details()) > 0 {
for _, detail := range s.Details() {
switch v := detail.(type) {
case *pbv1.ErrorCode:
ed.ErrorCode = v.GetErrorCode()
case *errdetails.LocalizedMessage:
if ed.GetMessage() != "" {
// Already set error message.
continue
}
if v.GetLocale() == appstatus.LocaleJaJp {
ed.Locale = v.GetLocale()
ed.Message = v.GetMessage()
}
}
}
} else {
ed.Message = s.Message()
}
e := pbv1.Error{
Error: ed,
}
// 省略...
}
status.FromError(err)
からgrpc.Status
を取得でき、Details
メソッドでWithDetails
に渡したproto.Message
を取得できます。
gRPC-Gateway
gRPC-GatewayでHTTPエラーを扱う関数を変更する必要があります。
runtime.HTTPError = HTTPError
まとめ
【前編】【後編】に続き、gRPC-Gatewayに関する実装で、カスタムエラーを扱う方法を紹介しました。
WithDetails
メソッドを使えば、紹介したフィールド以外でも返せるので、より詳細なエラーをクライアントに伝えることが可能になります。
また、カスタムエラーの記事はいくつかありますが、metadataでを使う方法が多く、おそらく古い方法なのでお勧めしません。
もし、他にいい実装方法を知っている方がいたら、コメント等でご紹介いただければ幸いです。