はじめに
最近筆者は GraphQL という言葉を耳にする機会が増えてきたなと感じています。新規開発として立ち上がったサービスでは GraphQL を使うというところも多いのではないでしょうか。
今回のこの記事では GraphQL サーバーにおいてエラーコードの概念を取り入れたときの実装 gqlgen における簡単な実装方法を紹介します。
この記事の話
- GraphQL でエラーコード周りどう実装するのか
- gqlgen で実装すると スキーマでエラーコードを実装して、SetErrorPresenter で整形する
GraphQL 上でエラーコードを使用する
GraphQL に限らず、REST など様々な形態の API ではクライアント側とエラーコードを予め決めておき、そのコードに応じてクライアント側の制御を行うといった手法が多く使われると思います。
具体的には発生するエラーに対して固有のコードを設定し、このエラーコードならこのメッセージを出そうとかこういう遷移をしようという使われ方をします。
Firebase では以下に示したリンク先のように固有のコードが定義されており、それに応じてクライアント側がどのようなエラーが発生したのか判断できるようになっています。
auth/claims-too-large setCustomUserClaims() に渡されたクレーム ペイロードが、最大許容サイズ(1,000 バイト)を超えています。
auth/email-already-exists 提供されたメールアドレスはすでに既存のユーザーによって使用されています。各ユーザーに固有のメールアドレスが必要です。
auth/id-token-expired 指定された Firebase ID トークンは期限切れです。
- 参考: Admin Authentication API エラー
このように発生しうるエラーに対してコードをつけることで何のエラーが発生しているのか明確にわかるようになり、利用するユーザーに対して次のアクションを取りやすくします。
このようなエラーコード構造を GraphQL で取り入れようとすると2種類の方法があると思います。(もっとあると思います。)
- エラーコードを全体共通のものとしてスキーマ上で定義する
- mutation や query などに応じてそれぞれの操作個別にエラー型をスキーマ上で定義する
どちらの方法においてもエラーコードという固有のコードを生成することに変わりありません。
GraphQL のスキーマでエラー型を定義し、そこに一つひとつエラーを定義します。エラーの型が1つであるため、そのエラーが複数の場所で発生する場合は、同じ内容のエラーでも違うエラーコードを生成する必要があります。
例えばAサービスのトークン有効切れとBサービスのトークン有効切れの違いがわかりやすいと思います。
どちらも有効期限切れなため、同じエラーコードでも良さそうですが、サービスが違うためエラーコードが(Firebaseの例を使用すると) a/id-token-expired
b/id-token-expired
のように複数設定する必要があります。(絶対にそうする必要はありません。)
このような形でエラーコードを GraphQL のスキーマ上で定義する場合以下のようになります。
type ErrorCode {
serviceA: ErrorCodeServiceAEnum
serviceB: ErrorCodeServiceBEnum
}
上記の例では ErrorCode
というエラーコードの型を定義し、その中に各サービスごとのエラーコード型を定義するようにしています。このようにすることでスキーマ上でエラーコードの定義をすることができます。
gqlgen でエラーコードを返す
実際にサーバー上で実装しようとするとどのようにすれば良いのでしょうか。
Go をよく使うので gqlgen というライブラリを使ってエラーコードを返すようにしたいと思います。
gqlgen ではエラーのハンドリングを SetErrorPresenter
によって実現できます。
GraphQL のハンドラーを呼び出すときにこの設定をしてあげることで return err してハンドラーでエラー検知したときにどのようにレスポンスとして返すか整形できるようになります。
func (g *GraphQLHandler) GraphQL(c echo.Context) error {
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: g.Resolver}))
//set error presenter
srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
// この辺で整形する処理を入れる
})
srv.ServeHTTP(c.Response(), c.Request())
*** 省略 ***
}
上記で例のコードを載せました。
簡単に解説すると srv.SetErrorPresenter の中で返すエラーを整形することでエラーコードをいい感じに返すことができます。
具体的に整形する処理は errors.Is などを使って整形するのがいいと思います。
var ex map[string]interface{}
if errors.Is(err, errUnauthorized) {
ex = map[string]interface{}{
"code": model.ErrorCodeServiceAEnumUnauthorized, // gqlgen で自動生成されたエラー
"status": http.StatusUnauthorized,
}
}
*** この辺に色々定義する ***
return &gqlerror.Error{
Message: err.Error(),
Path: graphql.GetPath(ctx),
Locations: nil,
Extensions: ex,
Rule: "",
}
本来はエラーコードだけ error.Is が並ぶと思いますが、一例だけ紹介します。ServiceA で発生した401エラーを返すときの例です。
このように extension でエラーコードと HTTP ステータスコードを返すのが良いと思います。
以下実際のレスポンスです。extensions にコードを入れることで制御がしやすくなったと思います。
{
"errors": [
{
"message": "hogehoge",
"path": [
"fugafuga"
],
"extensions": {
"code": "SERVICE_A_UNAUTHORIZED",
"status": 401
}
}
],
"data": null
}
最後に
ここまで見てくださりありがとうございました。
GraphQLではレスポンスのステータスコードだけではエラーハンドルすることが非常に難しいため、そこを補完するためにエラーコードの仕組み相当のものを入れた方が良いのではないかなと思っています。
ここで紹介したものは簡単なものなので、よりよい方法がたくさんあると思いますが、参考になれば幸いです。