we-promise/sure · error · Provider::Questrade::Error

network_error

network_error

Error message

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

What it means

Raised by Provider::Questrade's with_retries: the operation kept raising one of RETRYABLE_ERRORS (SocketError, Net::OpenTimeout, Net::ReadTimeout, ECONNRESET, ECONNREFUSED, ETIMEDOUT, EOFError) through MAX_RETRIES=3 attempts with exponential backoff plus jitter (2s, ~4s, ~8s, capped at 30s), after which Error(:network_error) replaces the last exception. The preceding 'Questrade API: ... failed after 3 retries' log line records the underlying class and message.

Source

Thrown at app/models/provider/questrade.rb:221

      begin
        yield
      rescue *RETRYABLE_ERRORS, RetryableResponseError => e
        retries += 1

        if retries <= max_retries
          delay = calculate_retry_delay(retries)
          Rails.logger.warn(
            "Questrade API: #{operation_name} failed (attempt #{retries}/#{max_retries}): " \
            "#{e.class}: #{e.message}. Retrying in #{delay}s..."
          )
          sleep(delay)
          retry
        else
          Rails.logger.error(
            "Questrade API: #{operation_name} failed after #{max_retries} retries: " \
            "#{e.class}: #{e.message}"
          )
          raise Error.new("Network error after #{max_retries} retries: #{e.message}", :network_error)
        end
      end
    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, 30 ].min
    end

    # Record provider failure detail to the super-admin /settings/debug log
    # (the sanctioned channel) instead of echoing the raw response body into
    # application logs or exception messages, where the importer re-logs it.
    def capture_response_error(reason, response)
      DebugLogEntry.capture(
        category: "provider_sync",
        level: "error",
        message: "Questrade API #{reason} (#{response.code})",

View on GitHub (pinned to e69894adb9)

Solutions

  1. Check the logged underlying exception class first — ECONNREFUSED vs SocketError vs ECONNRESET point to firewall vs DNS vs proxy respectively.
  2. Verify outbound connectivity from the runtime host: curl -v https://login.questrade.com and curl -v <api_server>/v1/accounts.
  3. If DNS (SocketError), fix resolver config in the container/cluster.
  4. If resets are proxy-induced, configure the proxy or TLS settings for the HTTParty client.
  5. If genuinely transient but longer than the window, schedule a later retry of the whole sync instead of widening MAX_RETRIES inline.

Example fix

# before
result = provider.get_balances(account_id: id)

# after
begin
  result = provider.get_balances(account_id: id)
rescue Provider::Questrade::Error => e
  raise if e.error_type != :network_error
  SyncJob.perform_in(15.minutes, item.id) # defer; don't hammer a dead network
end
Defensive patterns

Strategy: retry

Try / catch

begin
  provider.get_balances(account_id: id)
rescue Provider::Questrade::Error => e
  raise if e.error_type != :network_error
  SyncJob.perform_in(15.minutes, item.id) # internal retries already exhausted
end

Prevention

When it happens

Trigger: DNS failure resolving login.questrade.com or the per-session api_server host; firewall/security-group blocking outbound HTTPS; Questrade's api_server host unreachable from your region; connection resets on every attempt (some proxies kill the long-lived 120s-timeout requests); local dev machine offline.

Common situations: Deployed app missing outbound internet rules (ECONNREFUSED/ETIMEDOUT); corporate proxy MITM resetting TLS to Questrade; DNS misconfiguration in the container (SocketError); transient ISP-level outage lasting longer than the ~14s retry window.

Related errors


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