レッスン22:サービスへの接続時にエラーを処理する
外部APIからデータを取得する際、さまざまな種類のエラーが発生することがあります。
たとえば、次のクエリを考えてみましょう:
{
externalData: _sendJSONObjectItemHTTPRequest(
input: {
url: "https://newapi.getpop.org/wp-json/wp/v2/posts/8888/"
}
)
postTitle: _objectProperty(
object: $__externalData,
by: { path: "title.rendered"}
)
}インターネット接続が切断されると、フィールド _sendJSONObjectItemHTTPRequest がエラーをトリガーします:
{
"errors": [
{
"message": "cURL error 6: Could not resolve host: newapi.getpop.org (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://newapi.getpop.org/wp-json/wp/v2/posts/8888/",
"locations": [
{
"line": 2,
"column": 17
}
],
"extensions": {
"path": [
"externalData: _sendJSONObjectItemHTTPRequest(input: {url: \"https://newapi.getpop.org/wp-json/wp/v2/posts/8888/\"}) @export(as: \"externalData\")",
"query { ... }"
],
"type": "QueryRoot",
"field": "externalData: _sendJSONObjectItemHTTPRequest(input: {url: \"https://newapi.getpop.org/wp-json/wp/v2/posts/8888/\"}) @export(as: \"externalData\")",
"id": "root",
"code": "PoP/ComponentModel@e1"
}
},
{
"message": "Argument 'object' in field '_objectProperty' of type 'QueryRoot' cannot be null",
"locations": [
{
"line": 10,
"column": 13
}
],
"extensions": {
"path": [
"$__externalData",
"(object: $__externalData)",
"postTitle: _objectProperty(object: $__externalData, by: {path: \"title.rendered\"})",
"query { ... }"
],
"type": "QueryRoot",
"field": "postTitle: _objectProperty(object: $__externalData, by: {path: \"title.rendered\"})",
"id": "root",
"code": "gql@5.4.2.1[b]",
"specifiedBy": "https://spec.graphql.org/draft/#sec-Required-Arguments"
}
}
],
"data": {
"externalData": null,
"postTitle": null
}
}接続に成功しても、リクエストしたリソースが存在しない場合は 404 が返されます:
{
"errors": [
{
"message": "Client error: `GET https://newapi.getpop.org/wp-json/wp/v2/posts/8888/` resulted in a `404 Not Found` response:\n{\"code\":\"rest_post_invalid_id\",\"message\":\"Invalid post ID.\",\"data\":{\"status\":404}}\n",
"locations": [
{
"line": 2,
"column": 17
}
],
"extensions": {
"path": [
"externalData: _sendJSONObjectItemHTTPRequest(input: {url: \"https://newapi.getpop.org/wp-json/wp/v2/posts/8888/\"}) @export(as: \"externalData\")",
"query { ... }"
],
"type": "QueryRoot",
"field": "externalData: _sendJSONObjectItemHTTPRequest(input: {url: \"https://newapi.getpop.org/wp-json/wp/v2/posts/8888/\"}) @export(as: \"externalData\")",
"id": "root",
"code": "PoP/ComponentModel@e1"
}
},
{
"message": "Argument 'object' in field '_objectProperty' of type 'QueryRoot' cannot be null",
"locations": [
{
"line": 10,
"column": 13
}
],
"extensions": {
"path": [
"$__externalData",
"(object: $__externalData)",
"postTitle: _objectProperty(object: $__externalData, by: {path: \"title.rendered\"})",
"query { ... }"
],
"type": "QueryRoot",
"field": "postTitle: _objectProperty(object: $__externalData, by: {path: \"title.rendered\"})",
"id": "root",
"code": "gql@5.4.2.1[b]",
"specifiedBy": "https://spec.graphql.org/draft/#sec-Required-Arguments"
}
}
],
"data": {
"externalData": null,
"postTitle": null
}
}どちらの場合も、レスポンスに追加のエラーが発生していました:
{
"message": "Argument 'object' in field '_objectProperty' of type 'QueryRoot' cannot be null"
}このエラーが発生する理由は、最初のエラーの後、動的変数 $__externalData の値が null になり、2番目のエラーをトリガーするからです。これは理想的ではありません。エラーが発生したことを認識した上で、残りのGraphQLクエリの実行をスキップすることが望ましいでしょう。
このチュートリアルレッスンでは、それを実現する方法を探ります。
REST APIへの接続時にエラーを処理する
このGraphQLクエリはロジックを2つのオペレーションに分割します:
- 最初のオペレーションは動的変数
$requestProducedErrorsをエクスポートし、フィールド_sendJSONObjectItemHTTPRequestの値がnullかどうか(その場合、何らかのエラーが発生した)を示します - 2番目のオペレーションは
$requestProducedErrorsがtrueの場合に@skipされます
このようにして、実行するロジックを含む2番目のオペレーションは、最初のオペレーションでデータの取得中にエラーがあった場合にスキップされます:
query ConnectToRESTEndpoint($postId: ID!) {
endpoint: _sprintf(
string: "https://newapi.getpop.org/wp-json/wp/v2/posts/%s/?_fields=id,type,title,date"
values: [$postId]
) @remove
externalData: _sendJSONObjectItemHTTPRequest(
input: {
url: $__endpoint
}
) @export(as: "externalData")
requestProducedErrors: _isNull(value: $__externalData)
@export(as: "requestProducedErrors")
@remove
}
query ExecuteOperation
@depends(on: "ConnectToRESTEndpoint")
@skip(if: $requestProducedErrors)
{
# Do something...
postTitle: _objectProperty(
object: $externalData,
by: { path: "title.rendered"}
)
}$postId: 1 を渡すと、クエリは成功し、レスポンスは次のようになります:
{
"data": {
"externalData": {
"id": 1,
"date": "2019-08-02T07:53:57",
"type": "post",
"title": {
"rendered": "Hello world!"
}
},
"postTitle": "Hello world!"
}
}存在しないリソースに対して $postId: 8888 を渡すと、次のレスポンスが返されます(レスポンスに postTitle がなく、2番目のエラーメッセージもないことに注目してください):
{
"errors": [
{
"message": "Client error: `GET https://newapi.getpop.org/wp-json/wp/v2/posts/8888/?_fields=id,type,title,date` resulted in a `404 Not Found` response:\n{\"code\":\"rest_post_invalid_id\",\"message\":\"Invalid post ID.\",\"data\":{\"status\":404}}\n",
"locations": [
{
"line": 6,
"column": 17
}
],
"extensions": {
"path": [
"externalData: _sendJSONObjectItemHTTPRequest(input: {url: $__endpoint}) @export(as: \"externalData\")",
"query ConnectToRESTEndpoint($postId: ID!) { ... }"
],
"type": "QueryRoot",
"field": "externalData: _sendJSONObjectItemHTTPRequest(input: {url: $__endpoint}) @export(as: \"externalData\")",
"id": "root",
"code": "PoP/ComponentModel@e1"
}
}
],
"data": {
"externalData": null
}
}インターネット接続が切断されている場合は、次のレスポンスが返されます:
{
"errors": [
{
"message": "cURL error 6: Could not resolve host: newapi.getpop.org (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://newapi.getpop.org/wp-json/wp/v2/posts/8888/?_fields=id,type,title,date",
"locations": [
{
"line": 17,
"column": 17
}
],
"extensions": {
"path": [
"externalData: _sendHTTPRequest(input: {url: $__endpoint, method: GET}) { ... }",
"query ConnectToAPI($postId: ID!) @depends(on: \"ExportDefaultDynamicVariables\") { ... }"
],
"type": "QueryRoot",
"field": "externalData: _sendHTTPRequest(input: {url: $__endpoint, method: GET}) { ... }",
"id": "root",
"code": "PoP/ComponentModel@e1"
}
}
],
"data": {
"externalData": null
}
}REST APIレスポンスからエラーメッセージを表示する
前のクエリはフィールド _sendJSONObjectItemHTTPRequest を使用しており、ステータスコードが 200(またはその他の成功コード)であることを期待しています。
ただし、REST APIが存在しないリソースに対して 404 を返し、JSONレスポンスに説明的なエラーメッセージを提供することがあります。
_sendJSONObjectItemHTTPRequest を _sendHTTPRequest に置き換えることで、ウェブサーバーからのこのフィードバックをキャプチャし、GraphQLレスポンスの errors エントリに表示できます。
たとえば、WP REST APIの存在しないリソースからデータを取得すると、レスポンスに data.status エントリと関連データが返されます。
このGraphQLクエリはこのデータをキャプチャし、Response Error Trigger エクステンションが提供するフィールド _fail を使用して、レスポンスのエラーコードとメッセージを含むエラーエントリを明示的に追加します:
query ExportDefaultDynamicVariables
@configureWarningsOnExportingDuplicateVariable(enabled: false)
{
defaultEndpointHasErrors: _echo(value: true)
@export(as: "endpointHasErrors")
@remove
}
query ConnectToAPI($postId: ID!)
@depends(on: "ExportDefaultDynamicVariables")
{
endpoint: _sprintf(
string: "https://newapi.getpop.org/wp-json/wp/v2/posts/%s/?_fields=id,type,title,date"
values: [$postId]
) @remove
externalData: _sendHTTPRequest(
input: {
url: $__endpoint,
method: GET
}
) {
contentType
statusCode
body @remove
bodyJSONObject: _strDecodeJSONObject(string: $__body)
@export(as: "externalData")
}
isNullExternalData: _isNull(value: $__externalData)
@export(as: "isNullExternalData")
@remove
}
query ValidateAPIResponse
@depends(on: "ConnectToAPI")
@skip(if: $isNullExternalData)
{
endpointHasErrors: _propertyIsSetInJSONObject(
object: $externalData
by: {
path: "data.status"
}
)
@export(as: "endpointHasErrors")
@remove
}
query FailIfExternalAPIHasErrors($postId: ID!)
@depends(on: "ValidateAPIResponse")
@include(if: $endpointHasErrors)
@skip(if: $isNullExternalData)
{
code: _objectProperty(
object: $externalData,
by: {
key: "code"
}
) @remove
message: _objectProperty(
object: $externalData,
by: {
key: "message"
}
) @remove
errorMessage: _sprintf(
string: "[%s] %s",
values: [$__code, $__message]
) @remove
data: _objectProperty(
object: $externalData,
by: {
key: "data"
}
) @remove
_fail(
message: $__errorMessage
data: {
postId: $postId,
endpointData: $__data
}
) @remove
}
query ExecuteSomeOperation
@depends(on: "FailIfExternalAPIHasErrors")
@skip(if: $endpointHasErrors)
{
# Do something...
postTitle: _objectProperty(
object: $externalData,
by: { path: "title.rendered"}
)
}Response Error Trigger エクステンションは、errors の下にカスタムエントリを追加する2つの方法を提供します:
- フィールド
_failを使用する方法 - ディレクティブ
@failを使用する方法
フィールド _fail は常にエラーを追加しますが、ディレクティブ @fail は引数 condition の条件が満たされた場合にのみ追加します。デフォルト値は IS_NULL であり、適用されたフィールドの値が null の場合にトリガーされます:
query GetPost($id: ID!) {
post(by:{id: $id})
@fail(
message: "There is no post with the provided ID"
data: {
id: $id
}
)
{
id
title
}
}変数 $postId: 1 でクエリを実行するとリクエストは成功し、次の結果が得られます:
{
"data": {
"externalData": {
"contentType": "application/json; charset=UTF-8",
"statusCode": 200,
"bodyJSONObject": {
"id": 1,
"date": "2019-08-02T07:53:57",
"type": "post",
"title": {
"rendered": "Hello world!"
}
}
},
"postTitle": "Hello world!"
}
}変数 $postId: 8888 でクエリを実行するとリソースが存在せず、次の結果が得られます:
{
"errors": [
{
"message": "[rest_post_invalid_id] Invalid post ID.",
"locations": [
{
"line": 76,
"column": 3
}
],
"extensions": {
"path": [
"_fail(message: $__errorMessage, data: {postId: $postId, endpointData: $__data}) @remove",
"query FailIfExternalAPIHasErrors($postId: ID!) @depends(on: \"ValidateAPIResponse\") @include(if: $endpointHasErrors) @skip(if: $isNullExternalData) { ... }"
],
"type": "QueryRoot",
"field": "_fail(message: $__errorMessage, data: {postId: $postId, endpointData: $__data}) @remove",
"id": "root",
"failureData": {
"postId": 8888,
"endpointData": {
"status": 404
}
},
"code": "PoPSchema/FailFieldAndDirective@e1"
}
}
],
"data": {
"externalData": {
"contentType": "application/json; charset=UTF-8",
"statusCode": 404,
"bodyJSONObject": {
"code": "rest_post_invalid_id",
"message": "Invalid post ID.",
"data": {
"status": 404
}
}
}
}
}GraphQL APIへの接続時にエラーを処理する
GraphQL APIで存在しないリソースをクエリすると、レスポンスのステータスコードは 200 となり、そのリソースの値は null になります(404 を返すRESTとは異なります)。
以下のGraphQLクエリは、_sendGraphQLHTTPRequest の実行時にエラーが発生しなかったことを次の点を確認することで検証します:
- レスポンスが
nullでない(例:インターネット接続が切断されていない) - レスポンスに
errorsエントリが含まれていない - レスポンスの
data.postエントリの下にnullでない値が含まれている(つまり、クエリしたリソースが存在する)
query InitializeDynamicVariables
@configureWarningsOnExportingDuplicateVariable(enabled: false)
{
defaultResponseHasErrors: _echo(value: false)
@export(as: "responseHasErrors")
@remove
defaultPostIsMissing: _echo(value: false)
@export(as: "postIsMissing")
@remove
}
query ConnectToGraphQLAPI($postId: ID!)
@depends(on: "InitializeDynamicVariables")
{
externalData: _sendGraphQLHTTPRequest(
input: {
endpoint: "https://newapi.getpop.org/api/graphql/",
query: """
query GetPostData($postId: ID!) {
post(by: { id : $postId }) {
date
title
}
}
""",
variables: [
{
name: "postId",
value: $postId
}
]
}
) @export(as: "externalData")
requestProducedErrors: _isNull(value: $__externalData)
@export(as: "requestProducedErrors")
@remove
}
query ValidateResponse
@depends(on: "ConnectToGraphQLAPI")
@skip(if: $requestProducedErrors)
{
responseHasErrors: _propertyIsSetInJSONObject(
object: $externalData
by: {
key: "errors"
}
)
@export(as: "responseHasErrors")
@remove
postExists: _propertyIsSetInJSONObject(
object: $externalData
by: {
path: "data.post"
}
)
@remove
postIsMissing: _not(value: $__postExists)
@export(as: "postIsMissing")
@remove
}
query FailIfResponseHasErrors
@depends(on: "ValidateResponse")
@skip(if: $requestProducedErrors)
@skip(if: $postIsMissing)
@include(if: $responseHasErrors)
{
errors: _objectProperty(
object: $externalData,
by: {
key: "errors"
}
) @remove
_fail(
message: "Executing the GraphQL query produced error(s)"
data: {
errors: $__errors
}
) @remove
}
query ExecuteOperation
@depends(on: "FailIfResponseHasErrors")
@skip(if: $requestProducedErrors)
@skip(if: $responseHasErrors)
@skip(if: $postIsMissing)
{
# Do something...
postTitle: _objectProperty(
object: $externalData,
by: { path: "data.post.title" }
)
}$postId: 1 を渡すと、クエリは成功し、レスポンスは次のようになります:
{
"data": {
"externalData": {
"data": {
"post": {
"date": "2019-08-02T07:53:57+00:00",
"title": "Hello world!"
}
}
},
"postTitle": "Hello world!"
}
}存在しないリソースに対して $postId: 8888 を渡すと、次のレスポンスが返されます(レスポンスに postTitle がなく、エラーメッセージもないことに注目してください):
{
"data": {
"externalData": {
"data": {
"post": null
}
}
}
}インターネット接続が切断されている場合は、次のレスポンスが返されます:
{
"errors": [
{
"message": "cURL error 6: Could not resolve host: newapi.getpop.org (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://newapi.getpop.org/api/graphql/",
"locations": [
{
"line": 15,
"column": 17
}
],
"extensions": {
"path": [
"externalData: _sendGraphQLHTTPRequest(input: {endpoint: \"https://newapi.getpop.org/api/graphql/\", query: \"\n query GetPostData($postId: ID!) {\n post(by: { id : $postId }) {\n date\n title\n }\n }\n \", variables: [{name: \"postId\", value: $postId}]}) @export(as: \"externalData\")",
"query ConnectToGraphQLAPI($postId: ID!) @depends(on: \"InitializeDynamicVariables\") { ... }"
],
"type": "QueryRoot",
"field": "externalData: _sendGraphQLHTTPRequest(input: {endpoint: \"https://newapi.getpop.org/api/graphql/\", query: \"\n query GetPostData($postId: ID!) {\n post(by: { id : $postId }) {\n date\n title\n }\n }\n \", variables: [{name: \"postId\", value: $postId}]}) @export(as: \"externalData\")",
"id": "root",
"code": "PoP/ComponentModel@e1"
}
}
],
"data": {
"externalData": null
}
}リクエストしたリソースが存在しない場合にエラーを生成する
上記のGraphQLクエリでは、クエリした投稿が存在しない場合、null を返すだけで errors の下にエラーエントリはありません。
その状況でエラーを強制的に追加したい場合は、フィールド _fail を使用してエラーをトリガーする次のオペレーションを追加できます:
query FailIfPostNotExists($postId: ID!)
@skip(if: $requestProducedErrors)
@include(if: $postIsMissing)
@depends(on: "ValidateResponse")
{
errorMessage: _sprintf(
string: "There is no post with ID '%s'",
values: [$postId]
) @remove
_fail(
message: $__errorMessage
data: {
id: $postId
}
) @remove
}
query ExecuteOperation
@depends(on: [
"FailIfResponseHasErrors",
"FailIfPostNotExists"
])
# ...
{
# ...
}これで、存在しないリソースに対して $postId: 8888 を渡すと、次のレスポンスが返されます:
{
"errors": [
{
"message": "There is no post with ID '8888'",
"locations": [
{
"line": 96,
"column": 3
}
],
"extensions": {
"path": [
"_fail(message: $__errorMessage, data: {id: $postId}) @remove",
"query FailIfPostNotExists($postId: ID!) @skip(if: $requestProducedErrors) @include(if: $postIsMissing) @depends(on: \"ValidateResponse\") { ... }"
],
"type": "QueryRoot",
"field": "_fail(message: $__errorMessage, data: {id: $postId}) @remove",
"id": "root",
"failureData": {
"id": 8888
},
"code": "PoPSchema/FailFieldAndDirective@e1"
}
}
],
"data": {
"externalData": {
"data": {
"post": null
}
}
}
}