Skip to content

Commit bf1396c

Browse files
authored
fix: add retry logging and detailed error on exhaustion (#16)
Retry.with_retry/2 now logs each attempt with HTTP status, body, and backoff delay. Final error carries last_status and last_body instead of bare :max_retries_exceeded atom.
1 parent 90b2097 commit bf1396c

3 files changed

Lines changed: 38 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.1.10] - 2026-05-24
11+
12+
### Changed
13+
14+
- `Retry.with_retry/2` now logs each retry attempt with HTTP status, body, and backoff delay
15+
- Final exhaustion error changed from `{:api_error, :max_retries_exceeded}` to `{:api_error, :max_retries_exceeded, last_status, last_body}` — carries the last HTTP status and response body
16+
1017
## [0.1.9] - 2026-05-24
1118

1219
### Added
@@ -153,7 +160,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
153160
- **Async body drain**: Streaming errors drain the `Req.Response.Async` body at the adapter layer — callers never receive opaque structs
154161
- **Base URL handling**: Strips trailing `/v1` before appending API paths, correctly handles versioned paths (Z.AI `/v4`)
155162

156-
[Unreleased]: https://github.com/kakilangit/arcanum/compare/v0.1.9...HEAD
163+
[Unreleased]: https://github.com/kakilangit/arcanum/compare/v0.1.10...HEAD
164+
[0.1.10]: https://github.com/kakilangit/arcanum/compare/v0.1.9...v0.1.10
157165
[0.1.9]: https://github.com/kakilangit/arcanum/compare/v0.1.8...v0.1.9
158166
[0.1.8]: https://github.com/kakilangit/arcanum/compare/v0.1.7...v0.1.8
159167
[0.1.7]: https://github.com/kakilangit/arcanum/compare/v0.1.6...v0.1.7

lib/arcanum/retry.ex

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ defmodule Arcanum.Retry do
66
that adapters use for transient HTTP errors.
77
"""
88

9+
require Logger
10+
911
@max_attempts 3
1012
@max_backoff_ms :timer.seconds(30)
1113

@@ -36,6 +38,9 @@ defmodule Arcanum.Retry do
3638
- `:on_success` — `fn response -> result` for status 200 (required)
3739
- `:on_error` — `fn status, body -> result` for non-retriable errors (required)
3840
- `:max_attempts` — override default max attempts (optional)
41+
42+
On exhaustion, returns `{:error, {:api_error, :max_retries_exceeded, last_status, last_body}}`
43+
where `last_status` and `last_body` are from the final failed attempt.
3944
"""
4045
@spec with_retry(keyword(), (-> {:ok, map()} | {:error, term()})) :: term()
4146
def with_retry(opts, fun) do
@@ -62,18 +67,36 @@ defmodule Arcanum.Retry do
6267

6368
defp handle_non_200(opts, fun, attempt, max, retriable, on_error, status, body) do
6469
if Enum.member?(retriable, status) do
65-
maybe_retry(opts, fun, attempt, max)
70+
maybe_retry(opts, fun, attempt, max, status, body)
6671
else
6772
on_error.(status, body)
6873
end
6974
end
7075

71-
defp maybe_retry(_opts, _fun, attempt, max) when attempt >= max do
72-
{:error, {:api_error, :max_retries_exceeded}}
76+
defp maybe_retry(_opts, _fun, attempt, max, status, body) when attempt >= max do
77+
Logger.error(
78+
"Arcanum.Retry: all #{max} attempts exhausted (last: HTTP #{status}#{truncate_body(body)})"
79+
)
80+
81+
{:error, {:api_error, :max_retries_exceeded, status, body}}
7382
end
7483

75-
defp maybe_retry(opts, fun, attempt, max) do
76-
backoff(attempt)
84+
defp maybe_retry(opts, fun, attempt, max, status, body) do
85+
delay = min(:timer.seconds(2) * Integer.pow(2, attempt - 1), @max_backoff_ms)
86+
87+
Logger.warning(
88+
"Arcanum.Retry: attempt #{attempt}/#{max} failed with HTTP #{status}, " <>
89+
"retrying in #{div(delay, 1000)}s — #{truncate_body(body)}"
90+
)
91+
92+
Process.sleep(delay)
7793
do_retry(opts, fun, attempt + 1, max)
7894
end
95+
96+
defp truncate_body(body) when is_binary(body) and byte_size(body) > 200 do
97+
binary_part(body, 0, 200) <> "..."
98+
end
99+
100+
defp truncate_body(body) when is_binary(body), do: body
101+
defp truncate_body(body), do: inspect(body, limit: 200)
79102
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Arcanum.MixProject do
22
use Mix.Project
33

4-
@version "0.1.9"
4+
@version "0.1.10"
55
@source_url "https://github.com/kakilangit/arcanum"
66

77
def project do

0 commit comments

Comments
 (0)