Foundation.Retry.HTTP provides status-code classification, Retry-After
header parsing, and method-aware retry decisions for HTTP clients.
Status Classification
alias Foundation.Retry.HTTP
HTTP.retryable_status?(429) # true - rate limited
HTTP.retryable_status?(500) # true - server error
HTTP.retryable_status?(503) # true - service unavailable
HTTP.retryable_status?(408) # true - request timeout
HTTP.retryable_status?(409) # true - conflict
HTTP.retryable_status?(400) # false - client error
HTTP.retryable_status?(200) # false - successThe default set covers 408, 409, 429, and all 5xx codes.
Retry-After Parsing
Parse Retry-After headers into milliseconds:
# Delta-seconds
HTTP.parse_retry_after([{"Retry-After", "120"}])
# => 120_000
# HTTP-date (IMF-fixdate, RFC 850, asctime)
HTTP.parse_retry_after([{"Retry-After", "Wed, 31 Dec 2099 23:59:59 GMT"}])
# => milliseconds until that time
# Millisecond header (used by some APIs)
HTTP.parse_retry_after([{"retry-after-ms", "500"}])
# => 500
# Default when header is missing
HTTP.parse_retry_after([])
# => 1_000 (default)
HTTP.parse_retry_after([], 5_000)
# => 5_000 (custom default)Should-Retry Decisions
Check whether a response should be retried based on status and headers:
HTTP.should_retry?(%{status: 429, headers: []}) # true
HTTP.should_retry?(%{status: 200, headers: []}) # false
HTTP.should_retry?(503) # true
HTTP.should_retry?({429, [{"Retry-After", "60"}]}) # trueThe x-should-retry header overrides status-code classification:
HTTP.should_retry?(%{status: 500, headers: [{"x-should-retry", "false"}]})
# => false (header overrides)Method-Aware Rules
Different HTTP methods often have different retry semantics. For example,
you might retry GET on 500 but not POST:
rules = [
all: [429], # retry 429 for any method
get: [500, 502, 503], # retry server errors for GET
delete: [500, 502, 503] # retry server errors for DELETE
]
HTTP.should_retry_for_method?(:get, %{status: 503, headers: []}, rules)
# => true
HTTP.should_retry_for_method?(:post, %{status: 500, headers: []}, rules)
# => false (POST not in rules for 500)
HTTP.should_retry_for_method?(:post, %{status: 429, headers: []}, rules)
# => true (429 matches :all)Rules accept both keyword lists and maps, and methods can be atoms or strings.
Retry Delay Calculation
HTTP.retry_delay/1 provides exponential backoff with jitter, designed for
HTTP retry loops:
HTTP.retry_delay(0) # ~375-500ms
HTTP.retry_delay(1) # ~750-1000ms
HTTP.retry_delay(2) # ~1500-2000msCustomize the initial and max delays:
HTTP.retry_delay(0, 1_000, 30_000) # start at 1s, cap at 30sCombining with Retry
Wire HTTP helpers into a retry policy:
alias Foundation.{Backoff, Retry}
alias Foundation.Retry.HTTP
policy = Retry.Policy.new(
max_attempts: 5,
backoff: Backoff.Policy.new(strategy: :exponential, base_ms: 500, max_ms: 30_000),
retry_on: fn
{:ok, %{status: status}} -> HTTP.retryable_status?(status)
{:error, _} -> true
_ -> false
end,
retry_after_ms_fun: fn
{:ok, %{status: 429, headers: headers}} -> HTTP.parse_retry_after(headers)
_ -> nil
end
)
{result, _state} = Retry.run(fn -> make_request() end, policy)