Chapter 11

GraphQLサーバーから返却されるエラーメッセージ

さき(H.Saki)
さき(H.Saki)
2023.03.07に更新

この章について

GraphQLサーバーからは、いつもリクエストに応じたデータが得られるとは限りません。
サーバー内でエラーが発生した場合や、そもそもリクエストが不正なものだった場合には、エラーメッセージという形でそれがクライアントに提示されます。
この章では、GraphQLがユーザーに返すエラーデータについて深く掘り下げていきたいと思います。

GraphQLが返すエラー

エラーが持つフィールド

GraphQLクライアントがサーバーから受け取るエラーの形式は、github.com/vektah/gqlparser/v2/gqlerrorパッケージ内のError構造体として定義されています。

type Error struct {
	Message    string                 `json:"message"`
	Path       ast.Path               `json:"path,omitempty"`
	Locations  []Location             `json:"locations,omitempty"`
	Extensions map[string]interface{} `json:"extensions,omitempty"`
}

それぞれのフィールドの意味は以下の通りです。

  • Message: エラーの内容を表すメッセージフィールド
  • Path: GraphQLのクエリの中で、どのフィールドがエラーの原因になったのかを示す
  • Locations: GraphQLのクエリの中で、どの行の何文字目がエラーの原因になったのかを示す
  • Extensions: Messageの他にもエラーに関するメタ情報を付けたい場合に使う項目
Pathが含まれるエラー例
{
  "errors": [
    {
      "message": "fail to get projectV2 data",
      "path": [
        "user",
        "projectV2"
      ]
    }
  ],
  "data": {
    "user": null
  }
}
LocationsとExtensionsが含まれるエラー例
{
  "errors": [
    {
      "message": "Expected Name, found <EOF>",
      "locations": [
        {
          "line": 14,
          "column": 1
        }
      ],
      "extensions": {
        "code": "GRAPHQL_PARSE_FAILED"
      }
    }
  ],
  "data": null
}

リゾルバなどで発生したエラーは、どんなエラーであったとしても最終的にはこのgqlerror.Error構造体に変換された上でクライアントに渡ります。

複数個のエラーを返すパターン

ここで一つポイントとなるのが、「クライアントから見えるエラーは複数個になることがある」ということです。
例えば、以下のようなクエリを実行したとしましょう。

query {
  user(name: "hsaki") {
    id
    name
    projectV2(number: 1) {
      title
    }
    projectV2s(first: 2) {
      nodes {
        id
      }
    }
  }
}

このクエリを処理している最中に、projectV2projectV2sの二箇所でエラーが発生したとします。
すると、クライアントが得られるレスポンスは以下のような形になります。

{
  "errors": [
    {
      "message": "projectv2 err",
      "path": [
        "user",
        "projectV2"
      ]
    },
    {
      "message": "projectv2s err",
      "path": [
        "user",
        "projectV2s"
      ]
    }
  ],
  "data": {
    "user": null
  }
}

errorsというリストフィールドの中に、2種類のエラー情報が格納されていることがお分かりいただけるのではないでしょうか。

複数個のエラーをユーザーに返す方法

前述の例のように「projectV2projectV2sという2箇所別々のところ(=分割した別のリゾルバ内)でエラーが起きた」というような場合は、ユーザーに返すエラーも自然と2個になるのですが、1つのリゾルバの中で複数個のエラーを発生させたい場合にはどうすればいいでしょうか。

graph/schema.resolvers.go
// 返り値errorに複数個のエラーの情報を詰めたい
func (r *userResolver) ProjectV2(ctx context.Context, obj *model.User, number int) (*model.ProjectV2, error)

実は、その解決方法がGraphQLの公式Docに記載されています。
公式Docのコード例をそのまま引用する形で、やり方を紹介したいと思います。

graphql.AddError関数を使う

graphql.AddError関数をリゾルバの中で使うことで、ユーザーに返却するエラーを複数個追加することができます。

// DoThings add errors to the stack.
func (r Query) DoThings(ctx context.Context) (bool, error) {
	// Print a formatted string
	graphql.AddErrorf(ctx, "Error %d", 1)

	// Pass an existing error out
	graphql.AddError(ctx, gqlerror.Errorf("zzzzzt"))

	// Or fully customize the error
	graphql.AddError(ctx, &gqlerror.Error{
		Path:       graphql.GetPath(ctx),
		Message:    "A descriptive error message",
		Extensions: map[string]interface{}{
			"code": "10-4",
		},
	})

	// And you can still return an error if you need
	return false, gqlerror.Errorf("BOOM! Headshot")
}

gqlerror.List構造体を使う

gqlerror.Listは、複数個のエラーをまとめて1つのエラーとして扱うことができるエラー構造体です。
リゾルバの返り値エラーにこのgqlerror.List構造体を返すことで、クライアントに複数個のエラーを渡すことが可能です。

// DoThingsReturnMultipleErrors collect errors and returns it if any.
func (r Query) DoThingsReturnMultipleErrors(ctx context.Context) (bool, error) {
	errList := gqlerror.List{}
		
	// Add existing error
	errList = append(errList, gqlerror.Wrap(errSomethingWrong))

	// Create new formatted and append
	errList = append(errList, gqlerror.Errorf("invalid value: %s", "invalid"))

	// Or fully customize the error and append
	errList = append(errList, &gqlerror.Error{
		Path:       graphql.GetPath(ctx),
		Message:    "A descriptive error message",
		Extensions: map[string]interface{}{
			"code": "10-4",
		},
	})
	
	return false, errList
}