บทเรียนที่ 22: การจัดการข้อผิดพลาดเมื่อเชื่อมต่อกับบริการ
เราอาจพบข้อผิดพลาดประเภทต่างๆ เมื่อดึงข้อมูลจาก API ภายนอก
ตัวอย่างเช่น ลองพิจารณา query ต่อไปนี้:
{
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 ซึ่งทริกเกอร์ข้อผิดพลาดที่สอง สิ่งนี้ไม่เหมาะสม เราควรจะรับรู้ว่ามีข้อผิดพลาดเกิดขึ้น และจากนั้นข้ามการดำเนินการส่วนที่เหลือของ GraphQL query
ในบทเรียนนี้ เราจะมาสำรวจวิธีการทำสิ่งนี้
การจัดการข้อผิดพลาดเมื่อเชื่อมต่อกับ REST API
GraphQL query นี้แบ่งตรรกะออกเป็นสองออเปอเรชัน โดยที่:
- ออเปอเรชันแรกเอ็กซ์พอร์ตตัวแปรไดนามิก
$requestProducedErrorsซึ่งระบุว่าค่าของฟิลด์_sendJSONObjectItemHTTPRequestเป็นnullหรือไม่ (ในกรณีดังกล่าวคือมีข้อผิดพลาดเกิดขึ้น) - ออเปอเรชันที่สองจะถูก
@skipเมื่อ$requestProducedErrorsเป็นtrue
ด้วยวิธีนี้ ออเปอเรชันที่สองซึ่งมีตรรกะที่จะดำเนินการ จะถูกข้ามเมื่อมีข้อผิดพลาดในการดึงข้อมูลในออเปอเรชันแรก:
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 query จะสำเร็จ และการตอบกลับคือ:
{
"data": {
"externalData": {
"id": 1,
"date": "2019-08-02T07:53:57",
"type": "post",
"title": {
"rendered": "Hello world!"
}
},
"postTitle": "Hello world!"
}
}เมื่อส่ง $postId: 8888 ซึ่งเกี่ยวข้องกับรีซอร์สที่ไม่มีอยู่ เราจะได้รับการตอบกลับนี้ (สังเกตว่าไม่มี postTitle ในการตอบกลับ และไม่มีข้อความข้อผิดพลาดที่สอง):
{
"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
query ก่อนหน้านี้ใช้ฟิลด์ _sendJSONObjectItemHTTPRequest ซึ่งคาดหวังว่าสเตตัสโค้ดจะเป็น 200 (หรือโค้ดที่สำเร็จอื่นๆ)
อย่างไรก็ตาม เป็นไปได้ที่ REST API จะส่งคืน 404 สำหรับรีซอร์สที่ไม่มีอยู่ และให้ข้อความข้อผิดพลาดเชิงอธิบายในการตอบกลับ JSON
เราสามารถจับฟีดแบ็กนี้จากเว็บเซิร์ฟเวอร์ได้โดยการแทนที่ _sendJSONObjectItemHTTPRequest ด้วย _sendHTTPRequest และแสดงในเอนทรี errors ของการตอบกลับ GraphQL
ตัวอย่างเช่น เมื่อดึงข้อมูลจากรีซอร์สที่ไม่มีอยู่จาก WP REST API จะส่งคืนเอนทรี data.status ในการตอบกลับพร้อมข้อมูลที่เกี่ยวข้อง
GraphQL query นี้จับข้อมูลนี้ และเพิ่มเอนทรีข้อผิดพลาดอย่างชัดเจนพร้อมโค้ดและข้อความข้อผิดพลาดของการตอบกลับ โดยใช้ฟิลด์ _fail (จัดเตรียมโดยเอกซ์เทนชัน Response Error Trigger):
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:
- ผ่านฟิลด์
_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
}
}เมื่อดำเนินการ query ด้วยตัวแปร $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!"
}
}เมื่อดำเนินการ query ด้วยตัวแปร $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
เมื่อ query รีซอร์สที่ไม่มีอยู่ใน GraphQL API การตอบกลับจะมีสเตตัสโค้ด 200 และค่า null สำหรับรีซอร์สนั้น (ซึ่งแตกต่างจาก REST ที่ส่งคืน 404 แทน)
GraphQL ด้านล่างตรวจสอบว่าไม่มีข้อผิดพลาดเกิดขึ้นเมื่อดำเนินการ _sendGraphQLHTTPRequest โดยการตรวจสอบว่า:
- การตอบกลับไม่เป็น
null(เช่น การเชื่อมต่ออินเทอร์เน็ตไม่ขาดหาย) - การตอบกลับไม่มีเอนทรี
errors - การตอบกลับมีค่าที่ไม่เป็น
nullภายใต้เอนทรีdata.post(กล่าวคือ รีซอร์สที่ query มีอยู่)
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 query จะสำเร็จ และการตอบกลับคือ:
{
"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 query ด้านบน หากโพสต์ที่ query ไม่มีอยู่ มันจะส่งคืนเพียง 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
}
}
}
}