{"record":{"id":"af7ece103b78118e","repo":"we-promise/sure","slug":"request-failed-af7ece","errorCode":"request_failed","errorMessage":"Exception during GET request: #{e.message}","messagePattern":"Exception during GET request: #(.+?)","errorType":"exception","errorClass":"LunchflowError","httpStatus":null,"severity":"error","filePath":"app/models/provider/lunchflow.rb","lineNumber":26,"sourceCode":"  attr_reader :api_key, :base_url\n\n  def initialize(api_key, base_url: \"https://lunchflow.app/api/v1\")\n    @api_key = api_key\n    @base_url = base_url\n  end\n\n  # Get all accounts\n  # Returns: { accounts: [...], total: N }\n  def get_accounts\n    response = self.class.get(\n      \"#{@base_url}/accounts\",\n      headers: auth_headers\n    )\n\n    handle_response(response)\n  rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e\n    Rails.logger.error \"Lunch Flow API: GET /accounts failed: #{e.class}: #{e.message}\"\n    raise LunchflowError.new(\"Exception during GET request: #{e.message}\", :request_failed)\n  rescue => e\n    Rails.logger.error \"Lunch Flow API: Unexpected error during GET /accounts: #{e.class}: #{e.message}\"\n    raise LunchflowError.new(\"Exception during GET request: #{e.message}\", :request_failed)\n  end\n\n  # Get transactions for a specific account\n  # Returns: { transactions: [...], total: N }\n  # Transaction structure: { id, accountId, amount, currency, date, merchant, description, isPending }\n  def get_account_transactions(account_id, start_date: nil, end_date: nil, include_pending: false)\n    query_params = {}\n\n    if start_date\n      query_params[:start_date] = start_date.to_date.to_s\n    end\n\n    if end_date\n      query_params[:end_date] = end_date.to_date.to_s\n    end","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/lunchflow.rb#L8-L44","documentation":"Raised by get_accounts (and the other GET wrappers) when the request blows up outside handle_response: SocketError / Net::OpenTimeout / Net::ReadTimeout (network layer, beyond the 120s HTTParty timeout), or - via the blanket `rescue => e` - ANY other exception, including LunchflowErrors that handle_response already raised for HTTP 4xx/5xx. Those get re-wrapped, so an expired x-api-key (401) surfaces with this same 'Exception during GET request' message and :request_failed, losing its original error_type. The only way to tell them apart is the preceding log line ('... failed:' = network vs 'Unexpected error during...' = re-wrapped HTTP error).","triggerScenarios":"DNS/network failure reaching lunchflow.app (or a custom base_url); response slower than the 120s timeout; any HTTP error status from handle_response on GET /accounts, /accounts/{id}/transactions, /balance or /holdings being swallowed and re-raised as :request_failed by the broad rescue.","commonSituations":"Expired or wrong x-api-key appearing as a generic request failure, self-hosted/custom base_url typos, egress firewall blocking lunchflow.app, and developers debugging 'network' errors that were actually 401s.","solutions":["Check the Rails log to classify: 'GET /accounts failed: SocketError...' = genuine network issue; 'Unexpected error during GET /accounts: LunchflowError: Unauthorized' = re-wrapped HTTP error","Verify api_key and base_url first - auth failures masquerade as this error","Test connectivity: curl -H 'x-api-key: <key>' https://lunchflow.app/api/v1/accounts from the app host","Fix the masking: re-raise LunchflowError untouched before the generic rescue (see exampleFix), then handle the real error_type"],"exampleFix":"# app/models/provider/lunchflow.rb - every GET wrapper\n# before\n    handle_response(response)\n  rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e\n    raise LunchflowError.new(\"Exception during GET request: #{e.message}\", :request_failed)\n  rescue => e\n    raise LunchflowError.new(\"Exception during GET request: #{e.message}\", :request_failed)\n  end\n\n# after\n    handle_response(response)\n  rescue LunchflowError\n    raise # keep HTTP error_type (:unauthorized etc.) intact\n  rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e\n    raise LunchflowError.new(\"Exception during GET request: #{e.message}\", :request_failed)\n  rescue => e\n    raise LunchflowError.new(\"Exception during GET request: #{e.message}\", :request_failed)\n  end","handlingStrategy":"retry","validationCode":"# cheap reachability pre-flight (also catches base_url typos in self-hosted setups)\nrequire \"socket\"\ndef lunchflow_reachable?(base_url, timeout: 3)\n  uri = URI.parse(base_url)\n  TCPSocket.new(uri.host, uri.port || 443, connect_timeout: timeout).close\n  true\nrescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, IO::TimeoutError, URI::InvalidURIError\n  false\nend","typeGuard":"def lunchflow_request_failed?(error)\n  error.is_a?(Provider::Lunchflow::LunchflowError) && error.error_type == :request_failed\nend\n\ndef lunchflow_network_failure?(log_line)\n  log_line.include?(\"failed: SocketError\") || log_line.include?(\"failed: Net::\")\nend","tryCatchPattern":"# NOTE: until the rescue-wrap is fixed, check logs to separate network failures\n# from re-wrapped HTTP errors; both surface as :request_failed.\nbegin\n  provider.get_accounts\nrescue Provider::Lunchflow::LunchflowError => e\n  raise unless e.error_type == :request_failed\n  RetryableSync.schedule(account, wait: 5.minutes) # transient-first assumption\nend","preventionTips":["Verify api_key and base_url first - expired keys currently masquerade as :request_failed because of the blanket rescue","Add `rescue LunchflowError; raise` before the generic rescue in the provider to stop error_type loss","Allowlist lunchflow.app (or your custom base_url host) on egress firewalls","Smoke-test GET /accounts in CI to catch both auth and connectivity drift early"],"tags":["lunchflow","network","error-masking","rescue-wrap","httparty","request-failed"],"backgroundTag":"network-request-failure","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}