GraphQL: 200 OK! Tratamento de erros em GraphQL

oieduardorabelo

Eduardo Rabelo

Posted on January 28, 2020

GraphQL: 200 OK! Tratamento de erros em GraphQL

Como modelar efetivamente erros no seu esquema GraphQL

Todos sabemos o quão bom é o GraphQL quando as coisas vão bem, mas o que acontece quando as coisas dão errado?

Como lidamos com erros no GraphQL? Como podemos fazer isso de uma maneira fácil de entender?

Vamos começar executando uma consulta simples do GraphQL:

{
  user(username: "@ash") {
    id
    name
  }
}

podemos obter algo assim:

{
  "data": {
    "user": {
      "id": "268314bb7e7e",
      "name": "Ash Ketchum"
    }
  }
}

É isso que esperamos ver. Mas ao velejar, não é sempre que temos águas calmas. O que acontece quando fazemos essa mesma consulta, mas algo dá errado?

{
  "data": {
    "user": null
  },
  "errors": [
    { "path": [ "user" ],
      "locations": [ { "line": 2, "column": 3 } ],
      "extensions": {
        "message": "Object not found",
        "type": 2
      }
    }
  ]
}

Você recebe um erro. E você pode encontrá-los no errors array. Canonicamente, é aqui que os erros podem ser encontrados em uma resposta do GraphQL.

Há muito o que descompactar aqui. Podemos ver que ocorreu um erro quando consultamos o usuário. Podemos ver que a mensagem de erro era "Object not found" e onde estava localizada em nossa consulta.

O Problema

Então, o que há de errado nisso?

1. Todos os erros são tratados da mesma forma
Todos os erros acabam no array de erros, independentemente do tipo de erro.

2. É difícil saber de onde veio o erro
Nosso exemplo foi uma consulta simples, mas em consultas mais complexas é mais difícil saber de onde veio o erro, especialmente se houver, por exemplo, uma lista de itens.

3. É difícil para o cliente saber quais erros ele deve se importar.
Os clientes recebem todos os erros no array de erros, ficando difícil para os clientes procurarem por erros. Isso significa que os clientes não sabem quais casos existem, e muito menos quais são importantes, quais podemos ignorar etc.

E, por tudo isso, a resposta não ajuda o cliente à exibir algo útil para o usuário.

O que é um erro?

Antes de nos aprofundarmos nisso, devemos realmente entender o que é um erro.

Quando pensamos em um "erro", pensamos em coisas como Internal Server Error, Deleted User, Bad Gateway, Unavailable in Country, Suspended User, etc.

Mas parece que todos esses "erros" não são equivalentes.

Um Internal Server Error não parece o mesmo que um Suspended User.

O mesmo vale para um Bad Gateway vs Unavailable in Country. Parece que esses são tipos diferentes de erros.

Categorias de erro

Quando começamos a pensar em "erros" dessa maneira, podemos separá-los em categorias.

Quando fazemos isso, podemos ver coisas como Internal Server Error e Bad Gateway, e que algo realmente deu errado na solicitação. Então, esses são definitivamente erros.

Mas em casos como Deleted User, Unavailable in Country e Suspended User, nada deu errado. Eles não são o resultado que esperamos na maioria das vezes, mas certamente não são erros. São resultados.

Mas vamos analisar o porquê.

Erros

Se fizermos uma solicitação ao nosso servidor GraphQL, nosso servidor fará chamadas subseqüentes aos nossos serviços/back-end. Se um de nossos serviços lançar uma exceção, obteremos um HTTP 500 (ou equivalente) de volta e algo como "Server Error" será retornado em nosso array de erros.

Portanto, se recebermos um erro, isso significa que não conseguimos obter os dados solicitados, por algum motivo.

Resultados alternativos

Para "resultados alternativos", isso parece um pouco diferente. Temos nosso cliente e nosso cliente chama o endpoint /graphql. Quando nosso servidor graphql chama nossos serviços, nossos serviços podem enviar algo de volta como Unavailable in Country ou Suspended User.

Mas esses realmente não são erros, pois obtivemos os dados solicitados. Realmente, nossos "resultados alternativos" são apenas resultados.

Por exemplo, se solicitarmos um usuário, esperamos de volta o estado do usuário. Um usuário pode ser um usuário normal, mas não é Suspended User? E um Blocked User? Esses são apenas estados diferentes de um usuário. Então, não deveríamos ser capazes de realizar essas consultas se nos importamos com elas?

Se aprendemos uma coisa com GraphQL, é sempre possível consultar o que lhe interessa.

Modelando resultados no esquema GraphQL

Então, podemos modelar isso?

Digamos que temos um usuário. Também temos outros resultados sobre o que um usuário poderia ser. Estes são todos os resultados possíveis que temos quando consultamos um Usuário durante a operação normal.

Podemos usar uma Union (perfeito para representar qual dos vários estados!) Para criar um UserResult que englobe todos esses resultados possíveis.

Ao modelar dessa maneira, também podemos personalizar a aparência de cada resultado, dependendo do tipo de resultado que temos.

Por exemplo, se tivermos um Deleted User, poderíamos incluir um Message ou, se nosso usuário estiver Suspended, poderíamos incluir um Policy Violation Link.

No GraphQL SDL, pode ser algo como isto:

type User {
  id: ID!
  name: String
}
type Suspended {
  reason: String
}
type IsBlocked {
  message: String
  blockedByUser: User
}
type UnavailableInCountry {
  countryCode: Int
  message: String
}

O que nos permite criar um tipo de união como este:

union UserResult = User | IsBlocked | Suspended

Os clientes podems consultar os dados que precisam usar, portanto, se não conseguem lidar com um usuário suspenso, eles não o consultam e também devem ter algum comportamento de fallback ou um caso padrão.

Agora que esses estados diferentes estão no esquema (ao invés de implícitos em um erro), os clientes podem ver facilmente quais casos existem para tratar!

Então, quando fazemos uma consulta:

{
  userResult(username: "@ash") {
    __typename
    ... on User {
      id
      name
    }
    ... on IsBlocked {
      message
      blockedByUser {
        username
      }
    }
    ... on Suspended {
      reason
    }
}

Nós obtemos isso como resultado:

{
  "data": {
    "userResult": {
      "__typename": "User",
      "id": "268314bb7e7e",
      "name": "Ash Ketchum"
    }
  }
}

Como podemos ver, obtivemos uma resposta normal do usuário e sabemos que esse é um usuário devido ao campo __typename (as bibliotecas de clientes graphql tornam isso ainda mais fácil de manusear!).

Mas digamos que esse usuário não esteja realmente disponível. Se executarmos a mesma consulta novamente, obteremos isso como resultado:

{
  "data": {
    "userResult": {
      "__typename": "IsBlocked",
      "message": "User blocked: @ash",
        "blockedByUser": {
          "username": "@brock"
        }
     }
  }
}

Ao invés disso, recebemos IsBlocked e obtemos um resultado com todos o campos que solicitamos em uma resposta IsBlocked.

Isso é ótimo porque significa que:

1. Os resultados são personalizáveis ​​para cada entidade.
Um usuário terá resultados diferentes dos outros tipos, como o Tweet; podemos adicionar resultados diferentes e personalizar campos para cada tipo.

2. Sabemos de onde veio o erro
Sabemos exatamente de onde vem o erro na consulta (porque está anexado à entidade); na verdade, é codificado no esquema.

3. O cliente decide com quais erros se importa e quais erros pode ignorar.
O cliente pode consultar ou não consultar resultados diferentes, portanto decide o que é importante.

Estruturas complexas de esquema

Se usarmos essa maneira de modelar nossos resultados, podemos imaginar o uso de resultados para diferentes entidades na mesma consulta.

Portanto, além do Usuário e dos resultados que definimos anteriormente, também podemos definir o seguinte:

type ProfileImage {
  id: ID!
  value: String
}
type Flagged {
  reason: String
}
type Hidden {
  message: String
}

Diante disso, podemos definir nosso novo tipo de resultado:

union ImageResult = Image | Flagged | Hidden

Juntando tudo isso, agora podemos fazer uma consulta GraphQL que inclui os dois tipos de resultados que definimos:

{
  userResult(username: "@ash") {
  __typename
  ... on User {
    id
    name
    profileImageResult {
      __typename
      ... on Image {
        id
      }
      ... on Flagged {
        reason
      }
      ... on Hidden {
        message
      }
    }
  }
  ... on IsBlocked {
    message
    blockedByUser {
      username
    }
  }
}

Podemos obter algo assim:

{
  "data": {
    "userResult": {
      "__typename": "User",
      "id": "268314bb7e7e",
      "name": "Ash Ketchum",
      "profileImageResult": {
        "__typename": "Image",
        "id": "982642"
      }  
    }
  }
}

Ou, se a imagem solicitada for protegida por direitos autorais, obteremos algo assim de volta:

{
  "data": {
    "userResult": {
      "__typename": "User",
      "id": "268314bb7e7e",
      "name": "Ash Ketchum",
      "profileImageResult": {
        "__typename": "Flagged",
        "reason": "Copyrighted image"
      }  
    }
  }
}

Além de ter resultados personalizáveis ​​para cada entidade, saber de onde veio o erro e permitir que o cliente decida com quais erros ele se importa e quais pode ignorar, isso também significa que:

1. Os "erros" não causam falhas nas consultas aninhadas.
Conseguimos consultar dois tipos de resultados diferentes e, quando um "falhou", ele não jogou um erro em cascata e causou falha na consulta inteira.

2. Podemos ajustar o quão detalhado queremos que os resultados sejam.
Podemos adicionar tipos de resultado a qualquer entidade que desejamos (como fizemos com User e ProfileImage) ou NÃO! Temos que decidir. Novos tipos de resultados são adicionados ao esquema explicitamente, ao invés de se esconderem silenciosamente como novos tipos de erros.

3. Podemos representar com mais precisão nossos dados
Nosso esquema agora espelha a aparência de nossos dados, o que facilita a discussão.

E o mais interessante é que você pode personalizar isso o quanto for necessário.

Resultados

Algumas coisas a serem levadas em consideração ao pensar em modelar "erros" no GraphQL e apenas modelar em geral:

  1. Nem tudo é um erro
  2. Modele seus erros como erros, modele seus resultados reais como resultados
  3. Os resultados geralmente são coisas que você deseja exibir

O GraphQL intencionalmente não prescreve como modelar seu esquema, e o mesmo vale para erros e resultados. Cabe a você decidir o que melhor descreve seus dados.

Certamente não é aí que termina a modelagem de “erros” ou resulta no esquema. Como lidamos com a rate limiting? E os erros de autenticação ou autorização? Essas coisas pertencem ao esquema ou não?

Às vezes há uma resposta clara, e às vezes não há! Como você usa seus dados determina como você deve modelá-los.

Se você quiser saber tudo isso por meio de vídeo, ao invés de artigos, assista aqui, aqui ou aqui!


Créditos

💖 💪 🙅 🚩
oieduardorabelo
Eduardo Rabelo

Posted on January 28, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related