> For the complete documentation index, see [llms.txt](https://consumindo-apis-com-elixir.cafecomelixir.com.br/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://consumindo-apis-com-elixir.cafecomelixir.com.br/problemas-de-api-externa/rate-limite-de-curta-duracao/reexecutando-uma-requisicao.md).

# Reexecutando uma requisição

Alguns limitadores podem levar apenas alguns segundos para liberar, como por exemplo.

* Máximo de 3 requisições por segundo

Isso quer dizer que ao passar o segundo, teremos mais 10 requisições para fazer. Em termos de tempo, faz sentido aguardar o bloqueio. Adicionar esse tempo a requisição não impacta de forma tão negativa quanto receber uma mensagem de erro.&#x20;

Vamos primeiro criar nosso cenário de erro.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    # ...

    test "recovery of too much requests", %{bypass: bypass} do
      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        Plug.Conn.resp(conn, 429, "")
</strong>      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

<strong>      assert status == 200
</strong>    end
  end
end

</code></pre>

A ideia desse teste é conseguir se recuperar de um 429 utilizando a reexecução da requisição, esperando 1 segundos para acabar a penalidade. Obviamente, se rodar esse teste, receberemos um status 429.&#x20;

Para resolver esse problema, iremos utilizar um middleware do tesla, chamado [Tesla.Middleware.Retry](https://hexdocs.pm/tesla/Tesla.Middleware.Retry.html) onde passaremos os seguintes argumentos:

* ***delay*** -> Tempo de espera até próxima tentativa
* ***max\_retries*** -> Máximo de tentativas até retornar um erro
* ***max\_delay*** -> Máximo de tempo de espera
* ***should\_retry*** -> regra condicional para definir se uma requisição deve ou não reexecutar

Vamos lá. Não estamos mais usando a macro do Tesla, isso quer dizer, que não conseguiremos utilizar tiretamente o plug. Isso é bom, pode parecer mais trabalhoso, mas é bom. Para isso, precisamos definir os middlewares diretamente no [*Tesla.Client*](https://hexdocs.pm/tesla/Tesla.Client.html) passando os middlewares, que queremos. Até agora utilizamos o `Tesla.get/1`, passando por parâmetro o caminho completo de nosso *endpoint*. Porém, existe a função `Tesla.get/2`, onde o primeiro argumento é o `Client` e o segundo o *endpoint*. precisamos do client para configurar o middleware, usaremos ele.

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

<strong>    middlewares = [
</strong><strong>      {Tesla.Middleware.Retry,
</strong><strong>       delay: 1000,
</strong><strong>       max_retries: 3,
</strong><strong>       max_delay: 2_000,
</strong><strong>       should_retry: fn
</strong><strong>         {:ok, %{status: status}} when status in [429] -> true
</strong><strong>         {:ok, _} -> false
</strong><strong>         {:error, _} -> false
</strong><strong>       end}
</strong><strong>    ]
</strong>
    middlewares
<strong>    |> Tesla.client()
</strong>    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Conseguimos adicionar o middleware, mesmo tendo ficado meio cheio a função. Resolveremos isso depois. Agora você pode rodar seu teste.

Algo que devemos notar é a configuração should\_retry. É onde criamos a regra para rodar o *retry*. Ali temos a regra

* `{:ok, _} -> false` # Se nossa resposta for um :ok ele não deve rodar o retry
* `{:error, _} -> false` # Se for um :error não deve rodar (esse erro vem do Tesla, conexão estabelecida por alguma razão, seria um bom ponto para retry, mas não estamos vendo isso agora
* `{:ok, %{status: status}} when status in [429] -> true` # Esse estava no topo, mas coloquei por ultimo aqui para explicar melhor

Pegamos por pattern matching o status vindo do Tesla. Verificamos se esse status está dentro da lista ao fazer `status in [429]` caso esteja, queremos que faça o retry. Esta dentro de uma lista devido a poder colocar mais status ali. Um erro 500 seria legal, assim, alguns erros podem ser sanados sem nem transparecer para o usuário. Porem, estou me atentando apenas ao nosso inimigo 429.

Vamos rodar o teste.

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
```

<pre class="language-elixir"><code class="lang-elixir">> mix test
15:35:22.169 [error] #PID&#x3C;0.285.0> running Bypass.Plug (connection #PID&#x3C;0.283.0>, stream id 2) terminated
Server: localhost:3000 (http)
Request: GET /coffee/hot
** (exit) an exception was raised:
    ** (RuntimeError) route error
        (bypass 2.1.0) lib/bypass/plug.ex:28: Bypass.Plug.call/2
        (plug_cowboy 2.7.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_handler.erl
:37: :cowboy_handler.execute/2
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_stream_h.er
l:306: :cowboy_stream_h.execute/3
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_stream_h.er
l:295: :cowboy_stream_h.request_process/3
        (stdlib 5.2) proc_lib.erl:241: :proc_lib.init_p_do_apply/3


  1) test all_hot_coffees/0 recovery of too much requests (CoffeeShop.Integrations.Coffee.ClientTest)
     test/coffee_shop/integrations/coffee/client_test.exs:31
     match (=) failed
     code:  assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)
     left:  {:ok, %CoffeeShop.Integrations.Coffee.Response{status: status}}
     right: {
              :error,
              %CoffeeShop.Integrations.Coffee.Response{
<strong>                status: 500,
</strong>                body: %{},
                error: "Não foi possível se conectar ao serviço de cafés. Tento novamente mais tarde"
              }
            }
     stacktrace:
       test/coffee_shop/integrations/coffee/client_test.exs:40: (test)


Finished in 1.0 seconds (0.00s async, 1.0s sync)
3 tests, 1 failure, 2 excluded




</code></pre>

Recebemos um status 500. Isso foi uma falha em nosso serviço falso. O culpado é a função do `Bypass.expect_once/4` isso acontece por que o *Bypass* esperava apenas uma chamada dessa requisição (expect\_once), mas recebemos mais do que uma, comprovando que nosso *retry* funcionou.

*Bypass* possui a função `Bypass.expect/4` que funciona igual ao `expect_once/4`, mas sem o limitador de ser apenas uma vez. Vamos substituir ela por apenas `expect/4` e o resto continua igual

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    # ...
    
    test "too much requests", %{bypass: bypass} do
<strong>      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
</strong>        Plug.Conn.resp(conn, 429, "")
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

Se rodarmos o teste novamente, iremos perceber duas coisas.

1. O teste ainda falha (o que faz sentido, a resposta das duas requisições é o status 429
2. O teste ficou mais lento.

Vamos resolver a primeira parte. A resposta para as duas requisições em nosso serviço falso é está sendo o 429:&#x20;

```elixir
Plug.Conn.resp(conn, 429, "")
```

Isso quer dizer, nunca receberemos o 200 dessa forma. Precisamos criar um mecanismo que entenda a quantidade de requisições feitas nesse teste. Infelizmente nem o bypass nem o Tesla nos ajudam nessa hora. Não possui uma forma simples de saber, uma vez que o *conn* vindo do *bypass* não se comunica diretamente com a resposta do mesmo. Iremos precisar de algo que rode internamente no *bypass* e que possamos obter o resultado de contagem fora dele. Podemos usar um [Agent](https://hexdocs.pm/elixir/1.12.3/Agent.html).

Agents são uma abstração em torno de um GenServer. GenServer mantem estado e podemos resgatar pelo PID. Usando Agente, não precisamos do PID e sim, apenas da instância. Exatamente o que precisamos. Vamos usar o exemplo que tem na  documentação, é exatamente um contador =D

Criarei ele em `lib/integrations/couter.ex`

{% code title="lib/integrations/couter.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Counter do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, & &1)
  end

  def increment do
    Agent.update(__MODULE__, &(&1 + 1))
  end
end
```

{% endcode %}

Podemos agora usar as funções

* `Counter.start_link/1` para iniciar nosso processo
* `Counter.increment/0` para adicionar 1 ao contador
* `Couter.value/0` para obter o valor total incrementado

Vamos adicionar esse ao nosso teste.

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    test "too much requests", %{bypass: bypass} do
<strong>      Counter.start_link(0)
</strong>
      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        IO.inspect(Counter.value())
</strong>        
<strong>        Counter.increment()
</strong>        Plug.Conn.resp(conn, 429, "")
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

Os pontos importantes ali são.

* Iniciamos o *Counter* fora da chamada do *Bypass*, bem no inicio do teste.
* Incrementamos toda vez que o *Bypass* identificar uma nova requisição
* Adicionei também um `IO.inspect/1` para vermos o valor aparecer em nosso console.

Podemos rodar o teste agora e veremos o valor sendo incrementado

```
mix test
0
1
2
3


  1) test all_hot_coffees/0 too much requests (CoffeeShop.Integrations.Coffee.ClientTest)
     test/integrations/coffee/client_test.exs:45
     Assertion with == failed
     code:  assert status == 200
     left:  429
     right: 200
     stacktrace:
       test/integrations/coffee/client_test.exs:60: (test)

.....
Finished in 4.7 seconds (0.00s async, 4.7s sync)
1 doctest, 5 tests, 1 failure
```

A contagem ficou de 0, 1, 2 e 3, Isso quer dizer, o 0 foi a primeira requisição e quando ele falhou começou a processar os *retry*, tendo 3 chances para se recuperar, ao chegar no limite máximo ele simplesmente ficou com a resposta da última tentativa. Percebe como nossos *retries* estão funcionando bem? Falta apenas uma logica para mockarmos o retorno de uma desses retries de forma a ser um sucesso.

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    #...
    test "too much requests", %{bypass: bypass} do
      Counter.start_link(0)
      
      response =
        File.read!("test/coffee_shop/integrations/coffee/fixtures/success.json")

      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        case Counter.value() do
</strong><strong>          0 = _first_call ->
</strong><strong>            Counter.increment()
</strong><strong>            Plug.Conn.resp(conn, 429, "Too many requests")
</strong><strong>
</strong><strong>          1 = _first_retry_call ->
</strong><strong>            Plug.Conn.resp(conn, 200, response)
</strong><strong>        end
</strong>      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

O case é utilziado para saber qual resposta devolvemos. Simulamos ali, uma resposta com status 429 e em seguida, com o retry, uma nova resposta com o 200. Isso garante que o retry está funcionando e que temos tratamento para 429.

```
mix test
```

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 1.8 seconds (0.00s async, 1.8s sync)
1 doctest, 5 tests, 0 failures
```

Lindo não?

## O problema do Delay

Temos um pequeno problema no nosso código, para ser especifico, um problema em rodar os testes. Façamos um experimento. Entre no seu cliente e altere o valor do delay e max\_delay para 10\_000:

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

    middlewares = [
      {Tesla.Middleware.Retry,
<strong>       delay: 10_000,
</strong>       max_retries: 3,
<strong>       max_delay: 10_000,
</strong>       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Rode esse teste e veja que o teste irá passar.&#x20;

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 18.1 seconds (0.00s async, 18.1s sync)
1 doctest, 5 tests, 0 failures
```

Pareceu até ter travado certo? O teste demorou 18 segundos. Nosso *delay* esta funcionando corretamente, inclusive o teste passa como deveria. Mas isso causa um problema de fluxo de trabalho. Imagina termos mais 10 clientes que também precisamos fazer o teste de *retry*. Nossa suite de teste ficaria lenta.

Precisamos de uma forma de configurar o delay em nossos testes, para não comprometer nossa performance.

Um minuto de agradecimento a nós do passado, que criamos um mecanismo de configuração. Podemos então fazer o mesmo que fizemos com a configuração da URL base.

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")
<strong>    delay_to_retry = Keyword.get(opts, :delay_to_retry, 1000)
</strong>
    middlewares = [
      {Tesla.Middleware.Retry,
<strong>       delay: delay_to_retry,
</strong>       max_retries: 3,
       max_delay: 3000,
       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end
</code></pre>

Agora basta passar a configuração por parâmetro:

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    # ...

    test "too much requests", %{bypass: bypass} do
      Counter.start_link(0)

      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
        case Counter.value() do
          0 = _first_call ->
            Counter.increment()
            Plug.Conn.resp(conn, 429, "")

          1 = _first_retry_call ->
            Plug.Conn.resp(conn, 200, "")
        end
      end)

      opts = [
        base_url: "http://localhost:3000",
<strong>        delay_to_retry: 1
</strong>      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)
      assert status == 200
    end
  end
end
</code></pre>

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 1.0 seconds (0.00s async, 1.0s sync)
1 doctest, 5 tests, 0 failures
```

Teste finalizado em 1 segundo. Muito melhor.

## Refactoring

Por ultimo, vamos dar uma mexida em nosso cliente e deixa-lo melhor. Nossa função de requisição ficou grande e mistura chamada com configuração. Caso eu queira criar uma nova função para requisitar outro *endpoint*, teremos que duplicar a configuração, isso não é bom.&#x20;

Vamos resolver isso extraindo para uma função de criação de client. Vou chama-lo de `new_client/1` . Ela vai ser privada para apenas utilizarmos dentro do modulo. Para configurar o cliente, basta passar as opções de configurações por parâmetro. Sua resposta é um  `Tesla.Client` configurado que poderá ser usado em qualquer requisição.

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

<strong>  defp new_client(opts \\ []) do
</strong><strong>    middlewares = [
</strong><strong>      {Tesla.Middleware.Retry,
</strong><strong>       delay: Keyword.get(opts, :delay_to_retry, 1000),
</strong><strong>       max_retries: 3,
</strong><strong>       max_delay: 20_000,
</strong><strong>       should_retry: fn
</strong><strong>         {:ok, %{status: status}} when status in [429] -> true
</strong><strong>         {:ok, _} -> false
</strong><strong>         {:error, _} -> true
</strong><strong>       end}
</strong><strong>    ]
</strong><strong>
</strong><strong>    Tesla.client(middlewares)
</strong><strong>  end
</strong>
  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

    opts
<strong>    |> new_client()
</strong>    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Muito melhor. Também gosto de separar a URL base para facilitar leitura.&#x20;

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  defp new_client(opts \\ []) do
    middlewares = [
      {Tesla.Middleware.Retry,
       delay: Keyword.get(opts, :delay_to_retry, 1000),
       max_retries: 3,
       max_delay: 20_000,
       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
  end

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, base_url())

    opts
    |> new_client()
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
  
<strong>  defp base_url(), do: "https://api.sampleapis.com"
</strong>end

</code></pre>

Com isso, temos um código legivel e prático.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://consumindo-apis-com-elixir.cafecomelixir.com.br/problemas-de-api-externa/rate-limite-de-curta-duracao/reexecutando-uma-requisicao.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
