HTTPower.Retry (HTTPower v0.16.0)

View Source

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 retry attempts (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)

Retryable Errors

HTTP Status Codes:

  • 408 Request Timeout
  • 429 Too Many Requests
  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout

Transport Errors:

  • :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

Summary

Functions

Calculates exponential backoff delay with jitter.

Executes an HTTP request with retry logic.

Checks if an error reason is retryable.

Checks if an HTTP status code is retryable.

Functions

calculate_backoff_delay(attempt, retry_opts)

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(method, url, body, headers, adapter, opts)

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()} on success
  • {:error, HTTPower.Error.t()} on failure (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?(reason, retry_safe)

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?(status)

Checks if an HTTP status code is retryable.

Examples

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

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