LoginSignup
4

More than 3 years have passed since last update.

Go gRPC-Gatewayでカスタムエラーを返す方法

Last updated at Posted at 2019-12-22

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",
    },
))

これで、あるエラーにおいて日本語や英語のエラーメッセージを扱えることがわかりました。

エラーコード

エラーコードを扱うには、自前で用意する必要があるようです。

app/pb/v1/error.pb.go
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()
}

カスタムエラーの定義

ここまでのコードをまとめると、下記のようになります。

app/status/status.go
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エラーの実装

app/cmd/client/http_error.go
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エラーを扱う関数を変更する必要があります。

app/cmd/client/main.go
runtime.HTTPError = HTTPError

まとめ

【前編】【後編】に続き、gRPC-Gatewayに関する実装で、カスタムエラーを扱う方法を紹介しました。
WithDetailsメソッドを使えば、紹介したフィールド以外でも返せるので、より詳細なエラーをクライアントに伝えることが可能になります。

また、カスタムエラーの記事はいくつかありますが、metadataでを使う方法が多く、おそらく古い方法なのでお勧めしません。
もし、他にいい実装方法を知っている方がいたら、コメント等でご紹介いただければ幸いです。

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
4