ruby 122 lines · 3 tabs

HTTP Timeouts + Retries Wrapper (Faraday)

Shared by codesnips Jan 2026
3 tabs
require "faraday"
require "faraday/retry"

module Http
  class RetryableError < StandardError; end
  class CircuitOpenError < StandardError; end

  class HttpClientFactory
    RETRIABLE_EXCEPTIONS = [
      Faraday::TimeoutError,
      Faraday::ConnectionFailed,
      Faraday::RetriableResponse
    ].freeze

    def self.build(base_url:, open_timeout: 2, timeout: 5, max_retries: 3)
      Faraday.new(url: base_url) do |conn|
        conn.options.open_timeout = open_timeout
        conn.options.timeout = timeout

        conn.request :retry,
          max: max_retries,
          interval: 0.3,
          interval_randomness: 0.5,
          backoff_factor: 2,
          retry_statuses: [429, 502, 503, 504],
          methods: %i[get head options],
          exceptions: RETRIABLE_EXCEPTIONS,
          retry_if: ->(env, exc) { retriable?(env, exc) }

        conn.request :json
        conn.response :json, content_type: /\bjson$/
        conn.adapter Faraday.default_adapter
      end
    end

    def self.retriable?(env, exception)
      return true if RETRIABLE_EXCEPTIONS.any? { |k| exception.is_a?(k) }
      env && env.response && [429, 502, 503, 504].include?(env.response.status)
    end
  end
end
3 files · ruby Explain with highlit

This snippet shows how a resilient HTTP client is assembled around Faraday so that transient network failures don't cascade into user-facing errors. The core idea is that every outbound call must be bounded in time and retried only when it is safe to do so, with backoff and jitter to avoid retry storms.

In HttpClientFactory, a Faraday connection is built with an explicit options.open_timeout and options.timeout. The open timeout guards TCP connection establishment while the read timeout bounds how long the client waits for a response body; both are essential because a default Faraday connection can hang indefinitely on a slow peer. The faraday-retry middleware is configured with max attempts, exponential interval growth via backoff_factor, and interval_randomness to spread retries out. Critically, retry_statuses and methods restrict automatic retries to idempotent verbs and specific status codes, and retry_if inspects the exception so only timeouts and connection failures trigger another attempt — a POST that may have side effects is not blindly replayed.

The RetryableError and CircuitOpenError classes give callers a stable exception taxonomy independent of Faraday internals. PaymentGatewayClient wraps the raw connection and adds a lightweight circuit breaker: with_circuit tracks consecutive failures, trips to an open state once FAILURE_THRESHOLD is reached, and short-circuits calls for a cooldown window rather than hammering a downstream that is already struggling. This is the trade-off at the heart of resilience work — failing fast protects both services once retries alone stop helping.

The charge method demonstrates idempotency in practice: an Idempotency-Key header lets the gateway de-duplicate a retried request server-side, which is what makes retrying a POST acceptable at all. Faraday exceptions are translated into the local error types so the rest of the application never couples to the HTTP library.

Finally, ChargeCustomerJob shows the client used from a background job, where Sidekiq's own retry mechanism complements the in-process retries. A pitfall worth noting: in-library retries multiply with job-level retries, so timeouts and max must be tuned together to keep total latency bounded. This layered approach — timeouts, selective retries, backoff, and a breaker — is the standard pattern for any Rails service calling third-party APIs.


Related snips

Share this code

Here's the card — post it anywhere.

HTTP Timeouts + Retries Wrapper (Faraday) — share card
Link copied