we-promise/sure · error · Provider::Snaptrade::ApiError

Network error after #{max_retries} retries: #{e.message}

Error message

Network error after #{max_retries} retries: #{e.message}

What it means

Raised by the OAuth module's with_retries in Provider::Snaptrade: retryable Faraday network errors (TimeoutError, ConnectionFailed, ECONNRESET, ETIMEDOUT) persisted past max_retries attempts, each with a fixed sleep(delay) between tries (delay from exponential backoff with jitter). The final failure is logged and re-raised as a plain ApiError with no status_code, distinguishing a transport failure from an HTTP error response.

Source

Thrown at app/models/provider/snaptrade.rb:175

        begin
          yield
        rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::ETIMEDOUT => e
          retries += 1

          if retries <= max_retries
            delay = calculate_retry_delay(retries)
            Rails.logger.warn(
              "SnapTrade OAuth: #{operation_name} failed (attempt #{retries}/#{max_retries}): " \
              "#{e.class}: #{e.message}. Retrying in #{delay}s..."
            )
            sleep(delay)
            retry
          else
            Rails.logger.error(
              "SnapTrade OAuth: #{operation_name} failed after #{max_retries} retries: " \
              "#{e.class}: #{e.message}"
            )
            raise ApiError.new("Network error after #{max_retries} retries: #{e.message}")
          end
        end
      end

      # For requests that must not be replayed (single-use codes, token rotation):
      # translate a network failure into an ApiError without retrying.
      def without_retry(operation_name)
        yield
      rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::ETIMEDOUT => e
        Rails.logger.error("SnapTrade OAuth: #{operation_name} failed (not retried, non-idempotent): #{e.class}: #{e.message}")
        raise ApiError.new("Network error (not retried, non-idempotent request): #{e.message}")
      end

      def calculate_retry_delay(retry_count)
        base_delay = INITIAL_RETRY_DELAY * (2 ** (retry_count - 1))
        jitter = base_delay * rand * 0.25
        [ base_delay + jitter, MAX_RETRY_DELAY ].min
      end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Verify reachability from the app host: curl -v https://api.snaptrade.com (any HTTP status proves transport works)
  2. Re-run the operation - transient connectivity blips are what the retry loop exists for; sustained failure means an environment problem
  3. Check the warn logs for the exception class - ConnectionFailed points to DNS/firewall, TimeoutError to a slow or unresponsive endpoint
  4. If timeouts are chronic, review the timeout values on the oauth_connection (30s/10s) and any egress proxy in front of the app
Defensive patterns

Strategy: retry

Try / catch

begin
  oauth_client.token_request(...)
rescue Provider::Snaptrade::ApiError => e
  retry_later_with_backoff if e.status_code.nil? # transport failure, not an HTTP error
  raise
end

Prevention

When it happens

Trigger: Three-plus consecutive Faraday::TimeoutError/Faraday::ConnectionFailed while reaching api.snaptrade.com/oauth/* - DNS failure, blocked egress, or the endpoint not answering within the 30s connection timeout and 10s open timeout configured on the OAuth connection.

Common situations: Local network outage or VPN blocking api.snaptrade.com; SnapTrade API incident causing connect timeouts; CI environment without network access running OAuth code paths; slow proxy adding latency beyond the configured timeouts.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/1151de401c07323e. Report an issue: GitHub.