Apollo ClientはReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は公式サイトの「Handling operation errors」にもとづく、操作エラーをどう扱ったらよいかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」をお読みください)。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。
Apollo ClientがGraphQLサーバーで操作を実行するとき、さまざまなエラーに遭遇するかもしれません。Apollo Clientは、エラーの種類に応じて正しく扱えるよう、エラーが起こったら適切な情報を示します。
エラーの種類
リモートサーバーでGraphQLの操作を行ったとき、起こるかもしれないエラーはつぎのふたつです。
- GraphQLエラー
- ネットワークエラー
GraphQLエラー
GraphQL操作のサーバー側の実行に関わります。つぎのようなエラーです。
- 構文エラー: クエリの形式が正しくない場合など。
- 検証エラー: クエリに存在しないスキーマフィールドが含まれていた場合など。
- リゾルバエラー: クエリフィールドをつくろうとしてエラーが発生した場合など。
構文エラーか検証エラーが起きた場合、サーバーは操作をまったく行いません。操作が無効だからです。リゾルバエラーが生じたときは、サーバーは一部のデータを返すこともあります。GraphQLのエラーについて、詳しくは「Error handling」をご参照ください。
GraphQLエラーが発生した場合、サーバーはApollo Clientへのレスポンスのerrors
配列にそのエラーを含めます。つぎのコードはエラーレスポンスの例です。
{
"errors": [
{
"message": "Cannot query field \"nonexistentField\" on type \"Query\".",
"locations": [
{
"line": 2,
"column": 3
}
],
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"exception": {
"stacktrace": [
"GraphQLError: Cannot query field \"nonexistentField\" on type \"Query\".",
"...additional lines..."
]
}
}
}
],
"data": null
}
Apollo Clientは、これらのエラーをuseQuery
呼び出し(または用いた操作フック)が返したerror.graphQLErrors
配列に加えます。
GraphQLエラーによりApollo Serverが操作をまったくできない場合、4xx
ステータスコードで応答します。リゾルバエラーが生じたときは、レスポンスに一部でもデータを含んでいれば、応答のステータスコードは200
です。
リゾルバ エラーのある部分データ
リゾルバエラーを起こした操作でも、一部のデータが返されることもあります。操作の要求したデータすべてではなく一部がサーバーのレスポンスに含まれているということです。Apollo Clientはデフォルトでは部分データは無視します。この動作は後述の「GraphQLエラーポリシー」を設定することによりオーバーライド可能です。
ネットワークエラー
ネットワークエラーは、GraphQLサーバーと接続しようとしたときに起こります。通常、応答ステータスコードは、4xx
または5xx
です(データはなし)。
ネットワークエラーが生じると、Apollo ClientはそれをuseQuery
呼び出し(または用いた操作フック)が返したerror.networkError
フィールドに加えます。
再試行ロジックやその他の高度なネットワークエラー処理は、後述Apollo Linkを用いてアプリケーションに加えてください。
GraphQLエラーポリシー
GraphQL操作で1つ以上のリゾルバエラーが生じた場合、サーバーのレスポンスはdata
フィールドに一部のデータを含んでいるかもしれません。
{
"data": {
"getInt": 12,
"getString": null
},
"errors": [
{
"message": "Failed to get string!"
// ...その他のフィールド...
}
]
}
デフォルトでは、Apollo Clientは部分的なデータは破棄し、useQuery
呼び出し(または用いたフック)の結果にerror.graphQLErrors
配列がつくられます。部分的な結果を使うために定めるのが、操作のエラーポリシーです。
Apollo Clientは、つぎのような操作のエラーポリシーをサポートします。
ポリシー | 説明 |
---|---|
none |
レスポンスにGraphQLエラーが含まれていると、error.graphQLErrors として返される。レスポンスのdata は、サーバーがdata を返したとしても、undefined に定められる。結果として、ネットワークエラーとGraphQLエラーの結果がほぼ同じかたちの応答になる。デフォルトのエラーポリシー。 |
ignore |
graphQLErrors は無視される(graphQLErrors がつくられない)。返されたdata はキャッシュされて、エラーが生じなかったかのようにレンダリングされる。 |
all |
data とerror.graphQLErrors がともにつくられる。部分データとエラー情報のどちらもレンダリングできる。 |
エラーポリシーを設定する
エラーポリシーは、つぎのように操作フック(useQuery
など)に渡すオプションオブジェクトで定めます。
const MY_QUERY = gql`
query WillFail {
badField # このフィールドのリゾルバはエラーを起こした
goodField # このフィールドは正しくつくられた
}
`;
function ShowingSomeErrors() {
const { loading, error, data } = useQuery(MY_QUERY, { errorPolicy: 'all' });
if (loading) return <span>loading...</span>;
return (
<div>
<h2>Good: {data.goodField}</h2>
<pre>
Bad:{' '}
{error.graphQLErrors.map(({ message }, i) => (
<span key={i}>{message}</span>
))}
</pre>
</div>
);
}
このコード例で用いたエラーポリシーはall
です。部分データとエラー情報の両方を、可能であればレンダリングします。
Apollo Link による高度なエラー処理
Apollo Linkライブラリで設定できるのが、操作の実行中に発生したエラーの高度な処理です。
おすすめする最初のステップとして、onError
リンクをリンクチェーンに加えれば、エラーの詳細が受け取れ、それに応じて動作させれられます。
以下に示すコード例は、ApolloClient
コンストラクタに、ふたつのリンクが含まれたリンクチェーンを渡しています。
-
onError
:graphQLErrors
またはnetworkError
がサーバーのレスポンスにあるかどうか確かめる。エラーがあれば、その詳細はログに記録される。 -
HttpLink
: 各GraphQL操作をサーバーに送信する。- チェーンの終端リンク。
import { ApolloClient, HttpLink, InMemoryCache, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError) console.log(`[Network error]: ${networkError}`);
});
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' })
const client = new ApolloClient({
cache: new InMemoryCache(),
link: from([errorLink, httpLink]),
});
操作の再試行
Apollo Linkは、失敗した操作を再試行することで、解決できる可能性がある場合にも役立ちます。発生するエラーの種類に応じて、異なるリンクをお使いください。
-
onError
はGraphQLエラー。 -
RetryLink
はネットワークエラー。
GraphQLエラーの場合
onError
リンクは、返されたGraphQLエラーの種類にもとづいて、失敗した操作を再試行できます。たとえば、トークンベースの認証を使っている場合、トークンの有効期限が切れたときに自動的に再認証を処理することです。
操作を再試行するには、onError
関数の中からforward(operation)
を返してください。コード例はつぎのとおりです。
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
// AuthenticationErrorリゾルバにスローされると
// Apollo ServerはcodeにUNAUTHENTICATEDを設定
case 'UNAUTHENTICATED':
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
// リクエストを再試行してオブザーバブルが新たに返される
return forward(operation);
}
}
}
// ネットワークエラーの再試行にはonErrorリンクでなくRetryLinkが推奨
// ここではエラーのログのみ行う
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
[注記] 再試行した操作でさらにエラーが生じた場合、onError
リンクには渡されず、操作の無限ループは起こりません。つまり、onError
リンクは特定の操作を1度だけ再試行できるのです。
操作を再試行したくない場合は、onError
リンクの関数から何も返さないでください。
ネットワークエラーの場合
ネットワークエラーになった操作の再試行は、RetryLink
をリンクチェーンに加えるのがよいでしょう。このリンクを使えば、指数バックオフ(exponential backoff)や試行回数などの再試行ロジックが設定できます。詳しくは「Retry Link」をお読みください。
onError
リンクオプション
「Error Link」の「Options」をご参照ください。