Skip to content

Commit

Permalink
Merge branch 'master' into dependabot/hex/ibrowse-4.4.2
Browse files Browse the repository at this point in the history
  • Loading branch information
yordis authored Sep 10, 2023
2 parents 42a4892 + c04b571 commit 0adcd52
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 27 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ to use it in production environment as it does not validate SSL certificates
## Middleware

Tesla is built around the concept of composable middlewares.
This is very similar to how [Plug Router](https://github.com/elixir-plug/plug#the-plug-router) works.
This is very similar to how [Plug Router](https://github.com/elixir-plug/plug#plugrouter) works.

### Basic

Expand Down Expand Up @@ -389,7 +389,7 @@ defmodule Tesla.Middleware.SomeMiddleware do
### Examples
```
```elixir
defmodule MyClient do
use Tesla
Expand Down
2 changes: 1 addition & 1 deletion lib/tesla/builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ defmodule Tesla.Builder do
@doc """
Attach middleware to your API client.
```
```elixir
defmodule ExampleApi do
use Tesla
Expand Down
6 changes: 3 additions & 3 deletions lib/tesla/middleware.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ defmodule Tesla.Middleware do
or inside tuple in case of dynamic middleware (`Tesla.client/1`):
Tesla.client([{Tesla.Middleware.BaseUrl, "https://example.com"}])
## Ordering
The order in which middleware is defined matters. Note that the order when _sending_ the request
matches the order the middleware was defined in, but the order when _receiving_ the response
is reversed.
For example, `Tesla.Middleware.DecompressResponse` must come _after_ `Tesla.Middleware.JSON`,
otherwise the response isn't decompressed before it reaches the JSON parser.
Expand Down
4 changes: 4 additions & 0 deletions lib/tesla/middleware/json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ defmodule Tesla.Middleware.DecodeJson do
"""
@moduledoc since: "1.8.0"

@behaviour Tesla.Middleware

@impl Tesla.Middleware
def call(env, next, opts) do
opts = opts || []
Expand All @@ -180,6 +182,8 @@ defmodule Tesla.Middleware.EncodeJson do
"""
@moduledoc since: "1.8.0"

@behaviour Tesla.Middleware

@impl Tesla.Middleware
def call(env, next, opts) do
opts = opts || []
Expand Down
8 changes: 4 additions & 4 deletions lib/tesla/middleware/logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ defmodule Tesla.Middleware.Logger do
## Examples
```
```elixir
defmodule MyClient do
use Tesla
Expand All @@ -86,13 +86,13 @@ defmodule Tesla.Middleware.Logger do
It can be changed globally with config:
```
```elixir
config :tesla, Tesla.Middleware.Logger, format: "$method $url ====> $status / time=$time"
```
Or you can customize this setting by providing your own `format` function:
```
```elixir
defmodule MyClient do
use Tesla
Expand All @@ -114,7 +114,7 @@ defmodule Tesla.Middleware.Logger do
You can customize this setting by providing your own `log_level/1` function:
```
```elixir
defmodule MyClient do
use Tesla
Expand Down
57 changes: 49 additions & 8 deletions lib/tesla/middleware/retry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ defmodule Tesla.Middleware.Retry do
{:ok, _} -> false
{:error, _} -> true
end
# or
plug Tesla.Middleware.Retry, should_retry: fn
{:ok, %{status: status}}, _env, _context when status in [400, 500] -> true
{:ok, _reason}, _env, _context -> false
{:error, _reason}, %Tesla.Env{method: :post}, _context -> false
{:error, _reason}, %Tesla.Env{method: :put}, %{retries: 2} -> false
{:error, _reason}, _env, _context -> true
end
end
```
Expand All @@ -42,7 +50,9 @@ defmodule Tesla.Middleware.Retry do
- `:delay` - The base delay in milliseconds (positive integer, defaults to 50)
- `:max_retries` - maximum number of retries (non-negative integer, defaults to 5)
- `:max_delay` - maximum delay in milliseconds (positive integer, defaults to 5000)
- `:should_retry` - function to determine if request should be retried
- `:should_retry` - function with an arity of 1 or 3 used to determine if the request should
be retried the first argument is the result, the second is the env and the third is
the context: options + `:retries` (defaults to a match on `{:error, _reason}`)
- `:jitter_factor` - additive noise proportionality constant
(float between 0 and 1, defaults to 0.2)
"""
Expand All @@ -65,7 +75,7 @@ defmodule Tesla.Middleware.Retry do
delay: integer_opt!(opts, :delay, 1),
max_retries: integer_opt!(opts, :max_retries, 0),
max_delay: integer_opt!(opts, :max_delay, 1),
should_retry: Keyword.get(opts, :should_retry, &match?({:error, _}, &1)),
should_retry: should_retry_opt!(opts),
jitter_factor: float_opt!(opts, :jitter_factor, 0, 1)
}

Expand All @@ -84,15 +94,26 @@ defmodule Tesla.Middleware.Retry do
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.jitter_factor)
context = update_in(context, [:retries], &(&1 + 1))
retry(env, next, context)
else
res
{:arity, should_retry_arity} = :erlang.fun_info(context.should_retry, :arity)

cond do
should_retry_arity == 1 and context.should_retry.(res) ->
do_retry(env, next, context)

should_retry_arity == 3 and context.should_retry.(res, env, context) ->
do_retry(env, next, context)

true ->
res
end
end

defp do_retry(env, next, context) do
backoff(context.max_delay, context.delay, context.retries, context.jitter_factor)
context = update_in(context, [:retries], &(&1 + 1))
retry(env, next, context)
end

# Exponential backoff with jitter
defp backoff(cap, base, attempt, jitter_factor) do
factor = Bitwise.bsl(1, attempt)
Expand Down Expand Up @@ -124,6 +145,19 @@ defmodule Tesla.Middleware.Retry do
end
end

defp should_retry_opt!(opts) do
case Keyword.get(opts, :should_retry, &match?({:error, _}, &1)) do
should_retry_fun when is_function(should_retry_fun, 1) ->
should_retry_fun

should_retry_fun when is_function(should_retry_fun, 3) ->
should_retry_fun

value ->
invalid_should_retry_fun(value)
end
end

defp invalid_integer(key, value, min) do
raise(ArgumentError, "expected :#{key} to be an integer >= #{min}, got #{inspect(value)}")
end
Expand All @@ -134,4 +168,11 @@ defmodule Tesla.Middleware.Retry do
"expected :#{key} to be a float >= #{min} and <= #{max}, got #{inspect(value)}"
)
end

defp invalid_should_retry_fun(value) do
raise(
ArgumentError,
"expected :should_retry to be a function with arity of 1 or 3, got #{inspect(value)}"
)
end
end
8 changes: 4 additions & 4 deletions lib/tesla/mock.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Tesla.Mock do
## Setup
```
```elixir
# config/test.exs
config :tesla, adapter: Tesla.Mock
Expand All @@ -14,7 +14,7 @@ defmodule Tesla.Mock do
## Examples
```
```elixir
defmodule MyAppTest do
use ExUnit.Case
Expand All @@ -37,7 +37,7 @@ defmodule Tesla.Mock do
## Setting up mocks
```
```elixir
# Match on method & url and return whole Tesla.Env
Tesla.Mock.mock(fn
%{method: :get, url: "http://example.com/list"} ->
Expand Down Expand Up @@ -83,7 +83,7 @@ defmodule Tesla.Mock do
To solve this issue it is possible to setup a global mock
using `mock_global/1` function.
```
```elixir
defmodule MyTest do
use ExUnit.Case, async: false # must be false!
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
%{
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.2", "0c6292ecf3e3f20b7c88408f00096337c4bfd99bd46cc2fe63413ddbe45b3573", [:mix], [], "hexpm", "40b2dd2836199203df8500e4a270f10fc006cc95adc8a319e148dc3077391d96"},
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"con_cache": {:hex, :con_cache, "0.14.0", "863acb90fa08017be3129074993af944cf7a4b6c3ee7c06c5cd0ed6b94fbc223", [:mix], [], "hexpm", "50887a8949377d0b707a3c6653b7610de06074751b52d0f267f52135f391aece"},
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
Expand Down Expand Up @@ -28,7 +28,7 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
"mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"},
Expand Down
52 changes: 49 additions & 3 deletions test/tesla/middleware/retry_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Tesla.Middleware.RetryTest do
response =
case env.url do
"/ok" -> {:ok, env}
"/maybe" when retries == 2 -> {:error, :nxdomain}
"/maybe" when retries < 5 -> {:error, :econnrefused}
"/maybe" -> {:ok, env}
"/nope" -> {:error, :econnrefused}
Expand Down Expand Up @@ -39,9 +40,20 @@ defmodule Tesla.Middleware.RetryTest do
delay: 10,
max_retries: 10,
should_retry: fn
{:ok, %{status: status}} when status in [400, 500] -> true
{:ok, _} -> false
{:error, _} -> true
{:ok, %{status: status}}, _env, _context when status in [400, 500] ->
true

{:ok, _reason}, _env, _context ->
false

{:error, _reason}, %Tesla.Env{method: :post}, _context ->
false

{:error, _reason}, %Tesla.Env{method: :put}, %{retries: 2} ->
false

{:error, _reason}, _env, _context ->
true
end

adapter LaggyAdapter
Expand Down Expand Up @@ -74,6 +86,14 @@ defmodule Tesla.Middleware.RetryTest do
ClientWithShouldRetryFunction.get("/retry_status")
end

test "use custom retry determination function matching on env" do
assert {:error, :econnrefused} = ClientWithShouldRetryFunction.post("/maybe", "payload")
end

test "use custom retry determination function matching on context" do
assert {:error, :nxdomain} = ClientWithShouldRetryFunction.put("/maybe", "payload")
end

defmodule DefunctClient do
use Tesla

Expand Down Expand Up @@ -171,4 +191,30 @@ defmodule Tesla.Middleware.RetryTest do
ClientWithJitterFactorGt1.get("/ok")
end
end

test "ensures should_retry option is a function with arity of 1 or 3" do
defmodule ClientWithShouldRetryArity0 do
use Tesla
plug Tesla.Middleware.Retry, should_retry: fn -> true end
adapter LaggyAdapter
end

defmodule ClientWithShouldRetryArity2 do
use Tesla
plug Tesla.Middleware.Retry, should_retry: fn _res, _env -> true end
adapter LaggyAdapter
end

assert_raise ArgumentError,
~r/expected :should_retry to be a function with arity of 1 or 3, got #Function<\d.\d+\/0/,
fn ->
ClientWithShouldRetryArity0.get("/ok")
end

assert_raise ArgumentError,
~r/expected :should_retry to be a function with arity of 1 or 3, got #Function<\d.\d+\/2/,
fn ->
ClientWithShouldRetryArity2.get("/ok")
end
end
end

0 comments on commit 0adcd52

Please sign in to comment.