Lektion 22: Fehlerbehandlung bei der Verbindung zu Diensten
Beim Abrufen von Daten aus einer externen API können verschiedene Arten von Fehlern auftreten.
Betrachte beispielsweise den folgenden Query:
{
externalData: _sendJSONObjectItemHTTPRequest(
input: {
url: "https://newapi.getpop.org/wp-json/wp/v2/posts/8888/"
}
)
postTitle: _objectProperty(
object: $__externalData,
by: { path: "title.rendered"}
)
}Wenn die Internetverbindung unterbrochen wird, löst das Feld _sendJSONObjectItemHTTPRequest einen Fehler aus:
{
"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
}
}Wenn die Verbindung gelingt, die angeforderte Ressource aber nicht existiert, erhalten wir einen 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
}
}In beiden Fällen enthielt die Antwort einen zusätzlichen Fehler:
{
"message": "Argument 'object' in field '_objectProperty' of type 'QueryRoot' cannot be null"
}Dieser Fehler tritt auf, weil nach dem ersten Fehler die dynamische Variable $__externalData den Wert null hat, was den zweiten Fehler auslöst. Das ist nicht ideal; wir würden lieber erkennen, dass ein Fehler aufgetreten ist, und dann die Ausführung des restlichen GraphQL-Queries überspringen.
In dieser Tutorial-Lektion zeigen wir, wie du das erreichen kannst.
Fehlerbehandlung bei der Verbindung zu einer REST-API
Dieser GraphQL-Query teilt die Logik in zwei Operationen auf, wobei:
- Die erste Operation die dynamische Variable
$requestProducedErrorsexportiert und anzeigt, ob der Wert des Felds_sendJSONObjectItemHTTPRequestnullist (in diesem Fall ist ein Fehler aufgetreten) - Die zweite Operation mit
@skipübersprungen wird, wenn$requestProducedErrorstrueist
Auf diese Weise wird die zweite Operation, die die auszuführende Logik enthält, übersprungen, wenn beim Abrufen der Daten in der ersten Operation ein Fehler aufgetreten ist:
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"}
)
}Bei der Übergabe von $postId: 1 ist der Query erfolgreich und die Antwort lautet:
{
"data": {
"externalData": {
"id": 1,
"date": "2019-08-02T07:53:57",
"type": "post",
"title": {
"rendered": "Hello world!"
}
},
"postTitle": "Hello world!"
}
}Bei der Übergabe von $postId: 8888 für eine nicht vorhandene Ressource erhalten wir diese Antwort (beachte, dass in der Antwort kein postTitle vorhanden ist und auch keine zweite Fehlermeldung):
{
"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
}
}Wenn die Internetverbindung unterbrochen ist, erhalten wir diese Antwort:
{
"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
}
}Fehlermeldungen aus der REST-API-Antwort anzeigen
Der vorige Query verwendet das Feld _sendJSONObjectItemHTTPRequest, das erwartet, dass der Statuscode 200 ist (oder ein anderer Erfolgs-Code).
Es ist jedoch möglich, dass die REST-API für eine fehlende Ressource einen 404 zurückgibt und eine beschreibende Fehlermeldung in der JSON-Antwort liefert.
Wir können dieses Feedback vom Webserver erfassen, indem wir _sendJSONObjectItemHTTPRequest durch _sendHTTPRequest ersetzen, und es im Eintrag errors der GraphQL-Antwort anzeigen.
Wenn beispielsweise Daten von einer nicht vorhandenen Ressource der WP REST API abgerufen werden, gibt diese einen data.status-Eintrag in der Antwort mit zugehörigen Daten zurück.
Dieser GraphQL-Query erfasst diese Daten und fügt explizit einen Fehlereintrag mit dem Fehlercode und der Fehlermeldung der Antwort hinzu, indem das Feld _fail verwendet wird (bereitgestellt von der Erweiterung 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"}
)
}Die Erweiterung Response Error Trigger bietet zwei Möglichkeiten, einen benutzerdefinierten Eintrag unter errors hinzuzufügen:
- Über das Feld
_fail - Über die Direktive
@fail
Während das Feld _fail den Fehler immer hinzufügt, tut die Direktive @fail dies nur, wenn die Bedingung im Argument condition erfüllt ist. Der Standardwert ist IS_NULL, was bedeutet, dass sie ausgelöst wird, wenn das Feld, auf das sie angewendet wird, den Wert null hat:
query GetPost($id: ID!) {
post(by:{id: $id})
@fail(
message: "There is no post with the provided ID"
data: {
id: $id
}
)
{
id
title
}
}Wenn der Query mit der Variable $postId: 1 ausgeführt wird, ist die Anfrage erfolgreich und wir erhalten:
{
"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!"
}
}Wenn der Query mit der Variable $postId: 8888 ausgeführt wird, fehlt die Ressource und wir erhalten:
{
"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
}
}
}
}
}Fehlerbehandlung bei der Verbindung zu einer GraphQL-API
Wenn eine fehlende Ressource in einer GraphQL-API abgefragt wird, hat die Antwort den Statuscode 200 und den Wert null für diese Ressource (im Unterschied zu REST, das stattdessen einen 404 zurückgibt).
Der GraphQL-Query unten prüft, dass beim Ausführen von _sendGraphQLHTTPRequest keine Fehler aufgetreten sind, indem er verifiziert, dass:
- Die Antwort nicht
nullist (z. B.: die Internetverbindung nicht unterbrochen wurde) - Die Antwort keinen
errors-Eintrag enthält - Die Antwort einen nicht-
null-Wert unter dem Eintragdata.postenthält (d. h. die abgefragte Ressource existiert)
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" }
)
}Bei der Übergabe von $postId: 1 ist der Query erfolgreich und die Antwort lautet:
{
"data": {
"externalData": {
"data": {
"post": {
"date": "2019-08-02T07:53:57+00:00",
"title": "Hello world!"
}
}
},
"postTitle": "Hello world!"
}
}Bei der Übergabe von $postId: 8888 für eine nicht vorhandene Ressource erhalten wir diese Antwort (beachte, dass in der Antwort kein postTitle vorhanden ist und auch keine Fehlermeldung):
{
"data": {
"externalData": {
"data": {
"post": null
}
}
}
}Wenn die Internetverbindung unterbrochen ist, erhalten wir diese Antwort:
{
"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
}
}Einen Fehler erzeugen, wenn die angeforderte Ressource nicht existiert
Im obigen GraphQL-Query gibt es, wenn der abgefragte Beitrag nicht existiert, einfach null zurück und es gibt keinen Fehlereintrag unter errors.
Wenn wir in dieser Situation das Hinzufügen eines Fehlers erzwingen möchten, können wir die folgende Operation anhängen, die das Feld _fail verwendet, um einen Fehler auszulösen:
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"
])
# ...
{
# ...
}Wenn nun $postId: 8888 für eine nicht vorhandene Ressource übergeben wird, erhalten wir diese Antwort:
{
"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
}
}
}
}