     plug Tesla.Middleware.Retry,
       delay: 500,
       max_retries: 10,
+      max_delay: 4_000,
       should_retry: fn
         {:ok, %{status: status}} when status in [400, 500] -> true
         {:ok, _} -> false

  @behaviour Tesla.Middleware

  @moduledoc """
  Retry the HTTP call in case of connection error by default (`nxdomain`, `connrefused` etc).
  Application error checking for retry can be customized through `:should_retry` option by
  providing a function in returning a boolean.

  ### Example
  ```

  ```

  ### Options
  - `:delay`        - The base delay in milliseconds (defaults to 50)
  - `:max_retries`  - maximum number of retries (defaults to 5)
  - `:max_delay`    - maximum delay in milliseconds (defaults to 5000)
  - `:should_retry` - function to determine if request should be retried
  """

  @defaults [
    delay: 50,
    max_retries: 5,
    max_delay: 5_000
  ]

  @doc false
  def call(env, next, opts) do
    opts = opts || []

    context = %{
      retries: 0,
      delay: Keyword.get(opts, :delay, @defaults[:delay]),
      max_retries: Keyword.get(opts, :max_retries, @defaults[:max_retries]),
      max_delay: Keyword.get(opts, :max_delay, @defaults[:max_delay]),
      should_retry: Keyword.get(opts, :should_retry, &match?({:error, _}, &1))
    }

    retry(env, next, context)
  end

  # If we have max retries set to 0 don't retry
  defp retry(env, next, %{max_retries: 0}), do: Tesla.run(env, next)

  # If we're on our last retry then just run and don't handle the error
  defp retry(env, next, %{max_retries: max, retries: max}) do
    Tesla.run(env, next)
  end

  # Otherwise we retry if we get a retriable error
  defp retry(env, next, context) do
    res = Tesla.run(env, next)

    if context.should_retry.(res) do
      backoff(context.max_delay, context.delay, context.retries)
      context = update_in(context, [:retries], &(&1 + 1))
      retry(env, next, context)
    else
      res
    end
  end

  # Exponential backoff with jitter
  defp backoff(cap, base, attempt) do
    factor = :math.pow(2, attempt)
    max_sleep = trunc(min(cap, base * factor))
    delay = :rand.uniform(max_sleep)

    :timer.sleep(delay)
  end
end
