we-promise/sure · error · LunchflowError

request_failed

request_failed

Error message

Exception during GET request: #{e.message}

What it means

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).

Source

Thrown at app/models/provider/lunchflow.rb:26

  attr_reader :api_key, :base_url

  def initialize(api_key, base_url: "https://lunchflow.app/api/v1")
    @api_key = api_key
    @base_url = base_url
  end

  # Get all accounts
  # Returns: { accounts: [...], total: N }
  def get_accounts
    response = self.class.get(
      "#{@base_url}/accounts",
      headers: auth_headers
    )

    handle_response(response)
  rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
    Rails.logger.error "Lunch Flow API: GET /accounts failed: #{e.class}: #{e.message}"
    raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
  rescue => e
    Rails.logger.error "Lunch Flow API: Unexpected error during GET /accounts: #{e.class}: #{e.message}"
    raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
  end

  # Get transactions for a specific account
  # Returns: { transactions: [...], total: N }
  # Transaction structure: { id, accountId, amount, currency, date, merchant, description, isPending }
  def get_account_transactions(account_id, start_date: nil, end_date: nil, include_pending: false)
    query_params = {}

    if start_date
      query_params[:start_date] = start_date.to_date.to_s
    end

    if end_date
      query_params[:end_date] = end_date.to_date.to_s
    end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Check the Rails log to classify: 'GET /accounts failed: SocketError...' = genuine network issue; 'Unexpected error during GET /accounts: LunchflowError: Unauthorized' = re-wrapped HTTP error
  2. Verify api_key and base_url first - auth failures masquerade as this error
  3. Test connectivity: curl -H 'x-api-key: <key>' https://lunchflow.app/api/v1/accounts from the app host
  4. Fix the masking: re-raise LunchflowError untouched before the generic rescue (see exampleFix), then handle the real error_type

Example fix

# app/models/provider/lunchflow.rb - every GET wrapper
# before
    handle_response(response)
  rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
    raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
  rescue => e
    raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
  end

# after
    handle_response(response)
  rescue LunchflowError
    raise # keep HTTP error_type (:unauthorized etc.) intact
  rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
    raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
  rescue => e
    raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
  end
Defensive patterns

Strategy: retry

Validate before calling

# cheap reachability pre-flight (also catches base_url typos in self-hosted setups)
require "socket"
def lunchflow_reachable?(base_url, timeout: 3)
  uri = URI.parse(base_url)
  TCPSocket.new(uri.host, uri.port || 443, connect_timeout: timeout).close
  true
rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, IO::TimeoutError, URI::InvalidURIError
  false
end

Type guard

def lunchflow_request_failed?(error)
  error.is_a?(Provider::Lunchflow::LunchflowError) && error.error_type == :request_failed
end

def lunchflow_network_failure?(log_line)
  log_line.include?("failed: SocketError") || log_line.include?("failed: Net::")
end

Try / catch

# NOTE: until the rescue-wrap is fixed, check logs to separate network failures
# from re-wrapped HTTP errors; both surface as :request_failed.
begin
  provider.get_accounts
rescue Provider::Lunchflow::LunchflowError => e
  raise unless e.error_type == :request_failed
  RetryableSync.schedule(account, wait: 5.minutes) # transient-first assumption
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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