6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React + TypeScript: Apollo Clientで操作エラーを扱う

Posted at

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 dataerror.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」をご参照ください。

6
5
0

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
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?