{"record":{"id":"71c7666578edd6e9","repo":"we-promise/sure","slug":"network-error-71c766","errorCode":"network_error","errorMessage":"Network error after #{max_retries} retries: #{e.message}","messagePattern":"Network error after #(.+?) retries: #(.+?)","errorType":"exception","errorClass":"Provider::Questrade::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/questrade.rb","lineNumber":221,"sourceCode":"      begin\n        yield\n      rescue *RETRYABLE_ERRORS, RetryableResponseError => e\n        retries += 1\n\n        if retries <= max_retries\n          delay = calculate_retry_delay(retries)\n          Rails.logger.warn(\n            \"Questrade API: #{operation_name} failed (attempt #{retries}/#{max_retries}): \" \\\n            \"#{e.class}: #{e.message}. Retrying in #{delay}s...\"\n          )\n          sleep(delay)\n          retry\n        else\n          Rails.logger.error(\n            \"Questrade API: #{operation_name} failed after #{max_retries} retries: \" \\\n            \"#{e.class}: #{e.message}\"\n          )\n          raise Error.new(\"Network error after #{max_retries} retries: #{e.message}\", :network_error)\n        end\n      end\n    end\n\n    def calculate_retry_delay(retry_count)\n      base_delay = INITIAL_RETRY_DELAY * (2 ** (retry_count - 1))\n      jitter = base_delay * rand * 0.25\n      [ base_delay + jitter, 30 ].min\n    end\n\n    # Record provider failure detail to the super-admin /settings/debug log\n    # (the sanctioned channel) instead of echoing the raw response body into\n    # application logs or exception messages, where the importer re-logs it.\n    def capture_response_error(reason, response)\n      DebugLogEntry.capture(\n        category: \"provider_sync\",\n        level: \"error\",\n        message: \"Questrade API #{reason} (#{response.code})\",","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/questrade.rb#L203-L239","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the logged underlying exception class first — ECONNREFUSED vs SocketError vs ECONNRESET point to firewall vs DNS vs proxy respectively.","Verify outbound connectivity from the runtime host: curl -v https://login.questrade.com and curl -v <api_server>/v1/accounts.","If DNS (SocketError), fix resolver config in the container/cluster.","If resets are proxy-induced, configure the proxy or TLS settings for the HTTParty client.","If genuinely transient but longer than the window, schedule a later retry of the whole sync instead of widening MAX_RETRIES inline."],"exampleFix":"# before\nresult = provider.get_balances(account_id: id)\n\n# after\nbegin\n  result = provider.get_balances(account_id: id)\nrescue Provider::Questrade::Error => e\n  raise if e.error_type != :network_error\n  SyncJob.perform_in(15.minutes, item.id) # defer; don't hammer a dead network\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  provider.get_balances(account_id: id)\nrescue Provider::Questrade::Error => e\n  raise if e.error_type != :network_error\n  SyncJob.perform_in(15.minutes, item.id) # internal retries already exhausted\nend","preventionTips":["Verify outbound DNS/firewall from the deploy environment before first Questrade sync.","Let the job system reschedule rather than widening in-process retries past the SDK's 3 attempts.","Monitor which socket class actually failed (logged pre-raise) to distinguish env problems from incidents."],"tags":["questrade","network","retry-exhausted","timeout","connection-reset","brokerage-api"],"backgroundTag":"network-connection-failed","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}