Lição 22: Tratamento de erros ao conectar a serviços
Podemos encontrar diferentes tipos de erros ao buscar dados de uma API externa.
Por exemplo, considere a seguinte query:
{
externalData: _sendJSONObjectItemHTTPRequest(
input: {
url: "https://newapi.getpop.org/wp-json/wp/v2/posts/8888/"
}
)
postTitle: _objectProperty(
object: $__externalData,
by: { path: "title.rendered"}
)
}Se a conexão com a Internet cair, o campo _sendJSONObjectItemHTTPRequest acionará um erro:
{
"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
}
}Se conseguirmos nos conectar, mas o recurso solicitado não existir, receberemos um 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
}
}Em ambos os casos, havia um erro adicional na resposta:
{
"message": "Argument 'object' in field '_objectProperty' of type 'QueryRoot' cannot be null"
}Esse erro ocorre porque, após o primeiro erro, a variável dinâmica $__externalData terá valor null, acionando o segundo erro. Isso não é ideal; preferimos ser informados de que algum erro ocorreu e, então, pular a execução do restante da query GraphQL.
Nesta lição do tutorial, exploraremos como alcançar esse resultado.
Tratamento de erros ao conectar a uma API REST
Esta query GraphQL divide a lógica em duas operações, onde:
- A primeira operação exporta a variável dinâmica
$requestProducedErrors, indicando se o valor do campo_sendJSONObjectItemHTTPRequesténull(caso em que algum erro ocorreu) - A segunda operação é ignorada com
@skipquando$requestProducedErrorsétrue
Dessa forma, a segunda operação, que contém a lógica a ser executada, é pulada quando houve um erro ao buscar os dados na primeira operação:
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"}
)
}Ao passar $postId: 1, a query é bem-sucedida e a resposta é:
{
"data": {
"externalData": {
"id": 1,
"date": "2019-08-02T07:53:57",
"type": "post",
"title": {
"rendered": "Hello world!"
}
},
"postTitle": "Hello world!"
}
}Ao passar $postId: 8888 referente a um recurso inexistente, obtemos esta resposta (observe que não há postTitle na resposta e também nenhuma segunda mensagem de erro):
{
"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
}
}Se a conexão com a Internet estiver indisponível, obtemos esta resposta:
{
"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
}
}Exibindo as mensagens de erro da resposta da API REST
A query anterior utiliza o campo _sendJSONObjectItemHTTPRequest, que espera que o código de status seja 200 (ou qualquer outro código de sucesso).
No entanto, é possível que a API REST retorne um 404 para um recurso ausente e forneça uma mensagem de erro descritiva na resposta JSON.
Podemos capturar esse feedback do servidor web substituindo _sendJSONObjectItemHTTPRequest por _sendHTTPRequest e exibi-lo na entrada errors da resposta GraphQL.
Por exemplo, ao buscar dados de um recurso inexistente na WP REST API, ela retorna uma entrada data.status na resposta com os dados associados.
Esta query GraphQL captura esses dados e adiciona explicitamente uma entrada de erro com o código e a mensagem de erro da resposta, usando o campo _fail (fornecido pela extensão Acionador de Erros de Resposta):
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"}
)
}A extensão Acionador de Erros de Resposta fornece duas formas de adicionar uma entrada personalizada em errors:
- Via campo
_fail - Via diretiva
@fail
Enquanto o campo _fail adiciona o erro sempre, a diretiva @fail o faz apenas quando a condição no argumento condition é satisfeita. O valor padrão é IS_NULL, o que significa que será acionada quando o campo ao qual é aplicada tiver valor null:
query GetPost($id: ID!) {
post(by:{id: $id})
@fail(
message: "There is no post with the provided ID"
data: {
id: $id
}
)
{
id
title
}
}Ao executar a query com a variável $postId: 1 a requisição é bem-sucedida e obtemos:
{
"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!"
}
}Ao executar a query com a variável $postId: 8888 o recurso está ausente e obtemos:
{
"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
}
}
}
}
}Tratamento de erros ao conectar a uma API GraphQL
Ao consultar um recurso ausente em uma API GraphQL, a resposta terá código de status 200 e valor null para esse recurso (diferentemente do REST, que retorna um 404).
A query GraphQL abaixo valida que não ocorreram erros durante a execução de _sendGraphQLHTTPRequest verificando que:
- A resposta não é
null(ex.: a conexão com a Internet não caiu) - A resposta não contém a entrada
errors - A resposta contém um valor não
nullna entradadata.post(ou seja, o recurso consultado existe)
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" }
)
}Ao passar $postId: 1, a query é bem-sucedida e a resposta é:
{
"data": {
"externalData": {
"data": {
"post": {
"date": "2019-08-02T07:53:57+00:00",
"title": "Hello world!"
}
}
},
"postTitle": "Hello world!"
}
}Ao passar $postId: 8888 referente a um recurso inexistente, obtemos esta resposta (observe que não há postTitle na resposta e também nenhuma mensagem de erro):
{
"data": {
"externalData": {
"data": {
"post": null
}
}
}
}Se a conexão com a Internet estiver indisponível, obtemos esta resposta:
{
"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
}
}Gerando um erro se o recurso solicitado não existir
Na query GraphQL acima, se o post consultado não existir, ela simplesmente retorna null e não há nenhuma entrada de erro em errors.
Se quisermos forçar a adição de um erro nessa situação, podemos acrescentar a seguinte operação, que usa o campo _fail para acionar um erro:
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"
])
# ...
{
# ...
}Agora, ao passar $postId: 8888 referente a um recurso inexistente, obtemos esta resposta:
{
"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
}
}
}
}