Erro genérico

Quando nos conectamos a um serviço externo, abrimos uma brecha em nossas defesas. Podemos cair em cenários que não esperamos, como algum erro não mapeado ou até mesmo, o serviço parar de responder. Quando isso acontece, precisamos avisar nosso usuário elegantemente que algo deu errado.

Vamos a um exemplo prático. Nosso tratamento esta assim

lib/coffee_shop/integrations/coffee/response.ex
defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: %{}

  def build({:ok, %Tesla.Env{status: status, body: body}}) do
    response = %__MODULE__{
      status: status,
      body: JSON.decode!(body)
    }

    {:ok, response}
  end
end

Agora, vamos pensar. Caso algo de errado, ele vai entrar em nosso build/1, e fazer o JSON.decode!/1. Mas, e se o body, quando der erro, vir em um formato diferente? O que vai acontecer. Vamos atualizar nosso teste:

test/coffee_shop/integrations/coffee/response_test.exs
defmodule CoffeeShop.Integrations.Coffee.ResponseTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response

  describe "build/1" do
    # ...

    test "build an 500 error" do
      tesla_response = {:ok, %Tesla.Env{status: 500, body: "Server exploded"}}
      
      assert {:ok, %Response{body: _body}} = Response.build(tesla_response)
    end
  end
end

Criamos um teste simples com uma resposta em formato fora do padrão. Vamos rodar esse teste.

Recebemos um erro de unexpected token. Isso ocorre porque o JSON.decode!/1 nao conseguir fazer o parse de "Server exploded". Vamos tentar fazer na mao

Precisamos de uma estrutura em JSON, mas nosso serviço não trás esse formato para nós. Temos várias formas de lidar com isso. A mais simples é responder um erro genérico quando receber um status 500. Ou melhor, quando não receber o status esperado. Isso quer dizer, tudo o que não for esperado, vai cair no erro genérico, apenas para não gerar um erro incompreenssivel ou feio para o usuário.

Aqui é um bom local para mapearmos erros não conhecidos e ai sim, implementar uma solução para eles.

Entendido isso, vamos melhorar nosso teste. Nele precisamos saber que

  • Um erro ocorreu;

  • Mensagem para avisarmos o usuário.

No teste alteramos a assertion da resposta, onde esperamos agora um {:error, %Response{}} para refletir que um erro aconteceu. Tambem adicionamos o error sendo a mensagem que ele retorna.

Vamos em nosso response.ex atualizar a construção de nossa resposta

Adicionamos o status que podem realizar o build/1. Isso quer dizer, caso um status nao esteja no @success_status, ele não entrará na função.

Feito isso, precisamos criar uma função de mesmo nome para capturar todos os cenários restantes.

Adicionamos um guard clause na primeira função, liberando apenas para status 200. Todo o restante cairá na segunda função e irá gerar um erro genérico da integração. Tabém adicionamos uma mensagem padrão e retornamos ao invés de :ok, um :error para refletir que algo deu errado.

Criamos um mecanismo simples de controle de status, onde podemos gerar N cenários para a resposta que vem do nosso serviço e tratar da melhor forma que quisermos.

Ao rodar todos os testes, teremos uma quebra no client_test.exs, isso devido a resposta mudar para uma tupla com :error. Basta altera-la e tudo volta a funciona.

Feito. Agora temos outro problema para lidar.

Atualizado

Isto foi útil?