Entendendo os erros da API

Quando algo dá errado, a API responde com um array errors no corpo da resposta. Cada erro traz uma message e um objeto extensions, e é no extensions.code que está a informação que importa para decidir o que fazer.

Na maioria dos casos o status HTTP continua sendo 200, porque o erro é de execução da query, não do transporte. Só três situações fogem disso: requisição sem token autenticado (401), corpo com JSON inválido (400) e limite de requisições estourado (429).

Lendo a estrutura de um erro

{
  "data": null,
  "errors": [
    {
      "message": "Record not found",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["getPerson"],
      "extensions": { "code": "NOT_FOUND" }
    }
  ]
}
  • message é um texto fixo por tipo de erro. Não tente interpretá-lo, porque ele pode mudar de redação.
  • path aponta qual query ou mutation da sua requisição falhou. Numa requisição com várias operações, é como você sabe qual delas caiu.
  • extensions.code é o contrato estável. É por ele que você decide o tratamento.
  • data pode vir preenchido junto com o errors quando parte da requisição funcionou e parte falhou.

Conhecendo os códigos de erro

Código Quando acontece O que fazer
FORBIDDEN O usuário do token não tem permissão para a operação. Pare e reporte. Repetir a chamada devolve o mesmo erro.
UNPROCESSABLE Os dados enviados não passaram nas validações. Corrija os valores apontados e repita.
NOT_FOUND O registro informado não existe. Confira o id. Não é falta de permissão.
PLAN_EXCEEDED O plano do licenciado não libera o recurso. Pare e reporte. Não é falta de permissão.
RATE_LIMITED Você estourou o limite de requisições. Aguarde o tempo indicado e repita. Veja Limite de requisições da API.
sem code O valor de uma variável não é aceito pelo tipo. Corrija o valor. A própria resposta lista os valores válidos.

Tratando o FORBIDDEN

O FORBIDDEN é um erro de autorização, não de autenticação: seu token é válido, mas o usuário por trás dele não tem acesso àquela operação.

Quando a negativa vem de uma permissão de acesso, a resposta também diz por quê, em duas chaves que aparecem só quando são elas que barram o acesso:

extensions.required_scope
O nome técnico da permissão que está bloqueada para esse usuário. É o mesmo nome que você encontra no AccessScopeEnum do schema, com a descrição em português.

extensions.required_apps
A lista dos apps que a permissão exige, quando nenhum deles está instalado. Basta um estar ativo para liberar.

Exemplo de permissão bloqueada para o usuário:

{
  "data": null,
  "errors": [
    {
      "message": "The user does not have permission to perform this action",
      "path": ["getPerson"],
      "extensions": { "code": "FORBIDDEN", "required_scope": "person_show" }
    }
  ]
}

Aqui person_show é a permissão Pessoa → Consultar, na tela de Permissão de Acesso. Para resolver, alguém com acesso às configurações do sistema precisa liberar esse item para o usuário.

Exemplo de app obrigatório não instalado:

{
  "data": null,
  "errors": [
    {
      "message": "The user does not have permission to perform this action",
      "path": ["getMdfes"],
      "extensions": {
        "code": "FORBIDDEN",
        "required_apps": ["mdfe", "mdfe_acbr"]
      }
    }
  ]
}

A permissão está liberada para o usuário, mas o recurso depende de um app que o licenciado não tem instalado. Nesse caso liberar permissão não resolve: é preciso instalar um dos apps pela Loja de Apps.

As duas chaves são causas independentes, então podem vir juntas. Quando isso acontece, resolver só uma das duas não libera o acesso:

{
  "data": null,
  "errors": [
    {
      "message": "The user does not have permission to perform this action",
      "path": ["getMdfes"],
      "extensions": {
        "code": "FORBIDDEN",
        "required_scope": "mdfe_list",
        "required_apps": ["mdfe", "mdfe_acbr"]
      }
    }
  ]
}

Quando a negativa não vem de uma permissão de acesso (por exemplo, uma operação restrita a administradores), o erro traz só o código:

{
  "extensions": { "code": "FORBIDDEN" }
}

Não tente prever a permissão antes de chamar

O campo denied_access_scopes do usuário lista as permissões que estão bloqueadas para ele, e é tentador usá-lo para calcular por eliminação o que está liberado, comparando com a lista completa de permissões do schema. Não faça isso: a conta parece certa e erra em silêncio, porque uma permissão liberada ainda pode estar barrada por app não instalado, e porque o resultado envelhece no instante em que alguém muda a configuração.

O caminho confiável é o contrário: faça a chamada. Se o acesso não existe, a resposta é um FORBIDDEN que nomeia a causa, e isso já é suficiente para você saber que não deve seguir por ali e o que precisa reportar.

Tratando o UNPROCESSABLE

É o erro de dados. A message é sempre Unprocessable entity, e o que interessa está em extensions.errors: um objeto com uma entrada por atributo com problema, cada uma com a lista de mensagens em português.

{
  "data": { "createProduct": null },
  "errors": [
    {
      "message": "Unprocessable entity",
      "path": ["createProduct"],
      "extensions": {
        "code": "UNPROCESSABLE",
        "errors": {
          "gross_weight": ["não pode ficar em branco"]
        }
      }
    }
  ]
}

Quando o problema não é de um atributo específico, mas do registro como um todo, ele vem num atributo chamado base:

{
  "extensions": {
    "code": "UNPROCESSABLE",
    "errors": {
      "base": ["Não é possível excluir o registro pois existem vendas dependentes"]
    }
  }
}

E quando o erro está num item de uma coleção (os produtos de uma venda, por exemplo), a lista de erros segue a mesma ordem dos itens que você enviou, com um objeto vazio para os itens sem problema. No exemplo abaixo, o primeiro produto da venda é que está sem estoque:

{
  "extensions": {
    "code": "UNPROCESSABLE",
    "errors": {
      "invoice_products_attributes": [
        { "quantity": ["indisponível em estoque"] },
        {}
      ]
    }
  }
}

Use a posição para saber de qual item é o erro. Ela é a única ligação entre o erro e o item que você enviou.

Tratando valores fora do enum

Vários campos da API aceitam apenas um conjunto fechado de valores. Quando você envia um valor fora desse conjunto, a requisição é recusada antes de chegar ao sistema, e o erro não tem extensions.code: ele vem no formato nativo do GraphQL, com extensions.value e extensions.problems.

{
  "errors": [
    {
      "message": "Variable $attributes of type UserPermissionsInput was provided invalid value for denied_access_scopes.0 (Expected \"venda_alterar\" to be one of: activity_access, activity_create, ...)",
      "extensions": {
        "value": { "denied_access_scopes": ["venda_alterar"] },
        "problems": [
          {
            "path": ["denied_access_scopes", 0],
            "explanation": "Expected \"venda_alterar\" to be one of: activity_access, activity_create, ..."
          }
        ]
      }
    }
  ]
}

Esse erro é o mais fácil de resolver, porque a própria resposta lista os valores aceitos. O path dentro de problems aponta o campo e, em listas, a posição do valor recusado.

Atenção: um valor aceito pelo enum ainda pode ser recusado depois, por regra de negócio, e aí você recebe um UNPROCESSABLE. É o que acontece ao bloquear uma permissão de que outra permissão liberada depende: o valor existe, mas a combinação não é válida.

Tratando o NOT_FOUND

A message é sempre Record not found, sem dizer o tipo do registro, porque o path do erro já identifica a operação que falhou.

Vale reforçar a diferença: NOT_FOUND é registro inexistente, e FORBIDDEN é registro que você não pode ver. A API não devolve NOT_FOUND para esconder um registro que existe.

Tratando o PLAN_EXCEEDED

Acontece quando a operação esbarra num limite do plano do licenciado, e não numa permissão de usuário. A message já vem pronta para exibição, nomeando o recurso e o plano, e o extensions.landing_page_url traz a página com os detalhes do recurso (pode vir null).

{
  "data": { "createApp": null },
  "errors": [
    {
      "message": "A funcionalidade \"plataformas de vendas\" não está disponível para o plano Plano Grátis",
      "path": ["createApp"],
      "extensions": {
        "code": "PLAN_EXCEEDED",
        "landing_page_url": "https://facil123.com.br/planos"
      }
    }
  ]
}

Liberar permissões não resolve esse erro. A saída é mudar de plano.

Tratando as respostas que não são GraphQL

Três situações são recusadas antes da execução da query, e nelas o status HTTP é que carrega a informação:

401 Unauthorized
Token ausente, inválido ou revogado. Se você não enviar o header Accept: application/json, a resposta vira um redirecionamento (302) para a tela de login em vez de um 401. Veja Características do token de API.

400 Bad Request
O corpo da requisição não é um JSON válido. A resposta traz um errors com a mensagem começando em Invalid JSON in request body.

429 Too Many Requests
Limite de requisições estourado. A resposta traz o cabeçalho Retry-After com quantos segundos esperar. Veja Limite de requisições da API.

Perguntas e respostas

1 - Recebi um FORBIDDEN sem required_scope nem required_apps. O que faço?
R: Essa negativa não vem de uma permissão de acesso configurável, e sim de uma restrição da própria operação (as restritas a administradores, por exemplo). Não há item para liberar na tela de permissões.

2 - O status HTTP foi 200 mas veio um errors. A requisição funcionou ou não?
R: Não funcionou. Em GraphQL o status 200 diz apenas que a requisição chegou e foi processada. Sempre verifique a presença do array errors, não só o status.

3 - Posso repetir automaticamente uma chamada que falhou?
R: Só nos casos de RATE_LIMITED, respeitando o Retry-After. FORBIDDEN, PLAN_EXCEEDED, NOT_FOUND e UNPROCESSABLE devolvem o mesmo resultado em toda tentativa, e repetir só consome sua cota de requisições.