grpc-gatewayでMetadataに詰めたエラーの内容をJSONに詰めてRESTクライアントに返したい
引き続きgRPCの話。
gRPCでエラーをクライアントに返したい場合、通常だとステータスコードとエラーメッセージしか返せない。例えばアプリケーションレベルのバリデーションエラーみたいなものを返したい時、メルカリさんの資料によるとMetadataに詰めて送ると良いらしい、
gRPCがクライアントのときは詰めたMetadataを読み出せばいいんだけど、grpc-gatewayでRESTのクライアントに返す時にはどうしたらよいか調べた。
grpc-gatewayのエラーハンドリングをカスタマイズする
実はGitHubのwikiに How to customize your gateway
というのがあって、そこに結構色々と書かれている。
How to customize your gateway · grpc-ecosystem/grpc-gateway Wiki · GitHub
で、Wikiから辿った先にあるブログに実際に結構丁寧にエラーレスポンスのカスタマイズ方法が乗ってるので、それを参考にすればよい。
fun run() error { runtime.HTTPError = CustomHTTPError // 省略 } type errorBody struct { Error string `json:”error"` ErrorDetails []ErrorDetail `json:”errorDetails”` } type errorDetails struct { Field string `json:”field”` Message string `json:”message”` } // おもにgRPCのMetadataをerrorBodyのような構造に変換し、クライアントに返すためのカスタムハンドラー func CustomHTTPError(ctx context.Context, _ *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, _ *http.Request, err error) { const fallback = `{"error": "failed to marshal error message"}` w.Header().Set("Content-type", marshaler.ContentType()) w.WriteHeader(runtime.HTTPStatusFromCode(grpc.Code(err))) eb := errorBody{ Err: grpc.ErrorDesc(err), } md, _ := runtime.ServerMetadataFromContext(ctx) for k, v := range md.TrailerMD { eb.ErrorDetails = append(eb.ErrorDetails, errorDetail{ Field: strings.TrimSuffix(k, "-bin"), // バイナリで帰ってくる文字列は-binのprefixがつくので、クライアントが扱いやすいよう消す Message: v[0], // サーバー側では文字列としてしか入れていないが、何故かArrayで入ってくるの最初のものだけ取得する }) } jErr := json.NewEncoder(w).Encode(eb) if jErr != nil { w.Write([]byte(fallback)) } }
Go書くの久しぶりすぎてこんな感じでよかったか全然自信ないけど、一応やりたいことは実現できた。