{"record":{"id":"b8e80486c69888e3","repo":"we-promise/sure","slug":"network-error","errorCode":"network_error","errorMessage":"Network error after #{max_retries} retries: #{e.message}","messagePattern":"Network error after #(.+?) retries: #(.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/indexa_capital.rb","lineNumber":145,"sourceCode":"      begin\n        yield\n      rescue *RETRYABLE_ERRORS => e\n        retries += 1\n\n        if retries <= max_retries\n          delay = calculate_retry_delay(retries)\n          Rails.logger.warn(\n            \"IndexaCapital 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            \"IndexaCapital 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    def base_url\n      BASE_URL\n    end\n\n    def base_headers\n      {\n        \"Content-Type\" => \"application/json\",\n        \"Accept\" => \"application/json\"","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/indexa_capital.rb#L127-L163","documentation":"IndexaCapital's with_retries exhausted MAX_RETRIES=3 attempts with exponential backoff (2s base, cap 30s, jitter) on a transport error (SocketError, Net timeouts, ECONNRESET/REFUSED/ETIMEDOUT, EOFError) against https://api.indexacapital.com. It wraps any of the four public operations (list_accounts, get_holdings, get_portfolio, get_account_balance) and re-raises as Error(:network_error).","triggerScenarios":"DNS failure resolving api.indexacapital.com; firewall blocking egress to Indexa; repeated ReadTimeouts on /accounts/{n}/portfolio for large portfolios; a proxy resetting connections from the app host.","commonSituations":"Containerized deploys missing allowlist entries, transient Spanish-provider network issues outlasting the ~6s of cumulative backoff, local dev behind a VPN/proxy that resets long-lived TLS sessions.","solutions":["Confirm reachability from the app host: curl -v https://api.indexacapital.com/users/me (401 is fine - it proves the path works)","Check HTTP_PROXY/HTTPS_PROXY env vars and DNS resolution in the runtime environment","Let the scheduler retry on the next cycle; :network_error is never a credential problem, so do not lock the account","If timeouts dominate, raise the HTTParty timeout or fetch per-account data in smaller jobs"],"exampleFix":"# before\nprovider.get_holdings(account_number: num) # raises mid-loop, kills whole sync\n\n# after\nbegin\n  provider.get_holdings(account_number: num)\nrescue Provider::IndexaCapital::Error => e\n  next if e.error_type == :network_error # log and skip, retry next cycle\n  raise\nend","handlingStrategy":"retry","validationCode":"require \"socket\"\ndef indexa_reachable?(timeout: 3)\n  TCPSocket.new(\"api.indexacapital.com\", 443, connect_timeout: timeout).close\n  true\nrescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, IO::TimeoutError\n  false\nend","typeGuard":"def indexa_network_error?(error)\n  error.is_a?(Provider::IndexaCapital::Error) && error.error_type == :network_error\nend","tryCatchPattern":"begin\n  provider.get_portfolio(account_number: num)\nrescue Provider::IndexaCapital::Error => e\n  raise unless e.error_type == :network_error\n  skipped << account # log and continue the batch; retry next cycle\nend","preventionTips":["Allowlist api.indexacapital.com:443 on app hosts","Skip-and-record on :network_error in batch loops so one host-level outage doesn't kill the run","Never lock credentials or the account on :network_error - it is transport, not auth","Prefer fresh provider instances per run so stale memoized state can't compound"],"tags":["indexa-capital","network","retries-exhausted","dns","transport"],"backgroundTag":"connection-failed-after-retries","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}