# `HTTPower.Retry`
[🔗](https://github.com/mdepolli/httpower/blob/v0.22.0/lib/httpower/retry.ex#L1)

Retry logic with exponential backoff and jitter for HTTP requests.

Provides resilient HTTP execution by automatically retrying failed requests
with intelligent backoff strategies. Retry is an execution wrapper that sits
between the middleware pipeline and the HTTP adapter layer.

## How It Works

1. **Request Execution** - Calls the HTTP adapter
2. **Response Analysis** - Checks if the response/error is retryable
3. **Retry Decision** - Decides whether to retry based on:
   - HTTP status codes (408, 429, 500, 502, 503, 504)
   - Transport errors (timeout, closed, econnrefused, econnreset if safe)
   - Remaining retry attempts
4. **Backoff Calculation** - Calculates delay using exponential backoff with jitter
5. **Retry Execution** - Waits and retries the request

## Configuration

    # Global defaults (can be overridden per-request)
    HTTPower.get(url,
      max_retries: 3,         # Maximum retries after initial attempt (default: 3)
      retry_safe: false,      # Retry on connection reset (default: false)
      base_delay: 1000,       # Base delay in ms (default: 1000)
      max_delay: 30_000,      # Maximum delay cap in ms (default: 30000)
      jitter_factor: 0.2      # Jitter randomization 0.0-1.0 (default: 0.2)
    )

## Retry-After Header Support

For 429 (Too Many Requests) and 503 (Service Unavailable) responses,
HTTPower automatically respects the `Retry-After` header if present:

    # Server sends: Retry-After: 5
    # HTTPower waits exactly 5 seconds instead of exponential backoff
    {:ok, response} = HTTPower.get(url)

## What Gets Retried

**Retryable HTTP status codes** — the server responded, but the response indicates
a transient issue. After retries are exhausted, the final response is still returned
as `{:ok, response}` since the server did respond:
- 408 Request Timeout
- 429 Too Many Requests
- 500 Internal Server Error
- 502 Bad Gateway
- 503 Service Unavailable
- 504 Gateway Timeout

**Retryable transport errors** — the request never reached the server or the
connection was lost. After retries are exhausted, these return `{:error, error}`:
- `:timeout` - Request timeout
- `:closed` - Connection closed
- `:econnrefused` - Connection refused
- `:econnreset` - Connection reset (only if `retry_safe: true`)

## Exponential Backoff Formula

    delay = min(max_delay, base_delay * 2^(attempt-1)) * (1 - jitter * random())

Example delays (base_delay: 1000, jitter_factor: 0.2):
- Attempt 1: 800-1000ms
- Attempt 2: 1600-2000ms
- Attempt 3: 3200-4000ms

## Examples

    # Retry with custom configuration
    HTTPower.get("https://flaky-api.com",
      max_retries: 5,
      base_delay: 2000,
      max_delay: 60_000
    )

    # Enable retry on connection reset
    HTTPower.get("https://api.example.com",
      retry_safe: true
    )

    # Check if error is retryable
    HTTPower.Retry.retryable_status?(500)  # true
    HTTPower.Retry.retryable_status?(404)  # false

## Architecture Note

Retry is NOT implemented as middleware because:
- Middleware run BEFORE HTTP execution (request processing)
- Retry runs DURING HTTP execution (execution wrapper)
- Middleware run once, retry may execute multiple times
- This separation ensures middleware coordination works correctly:
  - Circuit breaker evaluates once per logical request (not per retry attempt)
  - Rate limiter consumes token once (retries don't consume extra tokens)
  - Dedup treats retries as same logical request

# `calculate_backoff_delay`

Calculates exponential backoff delay with jitter.

The delay increases exponentially with each attempt, capped at `max_delay`,
and randomized with jitter to prevent thundering herd.

## Parameters

- `attempt` - Current attempt number (1-based)
- `retry_opts` - Map with :base_delay, :max_delay, :jitter_factor

## Formula

    delay = min(max_delay, base_delay * 2^(attempt-1)) * (1 - jitter_factor * random())

## Examples

    iex> opts = %{base_delay: 1000, max_delay: 30_000, jitter_factor: 0.2}
    iex> HTTPower.Retry.calculate_backoff_delay(1, opts)
    800..1000  # Range due to jitter

    iex> HTTPower.Retry.calculate_backoff_delay(2, opts)
    1600..2000

    iex> HTTPower.Retry.calculate_backoff_delay(3, opts)
    3200..4000

# `execute_with_retry`

Executes an HTTP request with retry logic.

This is the main entry point called by `HTTPower.Client`. It wraps the
HTTP adapter call with retry logic and exponential backoff.

## Parameters

- `method` - HTTP method (:get, :post, :put, :delete)
- `url` - Request URL (URI struct or string)
- `body` - Request body (string or nil)
- `headers` - Request headers (map)
- `adapter` - HTTP adapter module or {module, config} tuple
- `opts` - Request options (includes retry configuration)

## Returns

- `{:ok, HTTPower.Response.t()}` - an HTTP response was received (any status code,
  including 5xx after retries are exhausted)
- `{:error, HTTPower.Error.t()}` - a transport/network error occurred after
  exhausting retries

## Examples

    HTTPower.Retry.execute_with_retry(
      :get,
      URI.parse("https://api.example.com"),
      nil,
      %{},
      HTTPower.Adapter.Finch,
      [max_retries: 3]
    )

# `retryable_error?`

Checks if an error reason is retryable.

Takes into account the `retry_safe` configuration for connection reset errors.

## Parameters

- `reason` - Error reason (HTTP status tuple, transport error, or atom)
- `retry_safe` - Whether to retry on connection reset (boolean)

## Examples

    iex> HTTPower.Retry.retryable_error?({:http_status, 500, response}, false)
    true

    iex> HTTPower.Retry.retryable_error?(:timeout, false)
    true

    iex> HTTPower.Retry.retryable_error?(:econnreset, false)
    false

    iex> HTTPower.Retry.retryable_error?(:econnreset, true)
    true

# `retryable_status?`

Checks if an HTTP status code is retryable.

## Examples

    iex> HTTPower.Retry.retryable_status?(500)
    true

    iex> HTTPower.Retry.retryable_status?(404)
    false

---

*Consult [api-reference.md](api-reference.md) for complete listing*
