we-promise/sure · error · Provider::Snaptrade::ApiError
SnapTrade positions response has no results array (keys: #{r
Error message
SnapTrade positions response has no results array (keys: #{response.is_a?(Hash) ? response.keys.inspect : response.class}) What it means
Raised by Provider::Snaptrade#get_positions when the /positions/all response is a Hash that lacks a 'results' Array (or is not a Hash at all). This is an intentional guard: an empty results array is a legitimately empty account, but a missing key means a partial or schema-changed response, and raising prevents the caller from overwriting the last good snapshot with an empty one. The message lists the actual response keys (or class) for diagnosis.
Source
Thrown at app/models/provider/snaptrade.rb:224
def list_accounts
get_json("/api/v1/accounts")
end
# Returns Array<Hash> of balance entries
def get_balances(account_id:)
get_json("/api/v1/accounts/#{account_id}/balances")
end
# Returns Array<Hash> of positions
def get_positions(account_id:)
response = get_json("/api/v1/accounts/#{account_id}/positions/all")
results = response["results"] if response.is_a?(Hash)
# An empty `results` is a legitimately empty account, but a missing one is
# a partial or schema-changed response. Raising leaves the previous
# snapshot in place rather than overwriting it with nothing.
unless results.is_a?(Array)
raise ApiError.new(
"SnapTrade positions response has no results array " \
"(keys: #{response.is_a?(Hash) ? response.keys.inspect : response.class})"
)
end
results.reject { |position| unsupported_instrument?(position) }
end
# Returns raw JSON: paginated form is {"data" => [...]}, may also be a plain Array
def get_account_activities(account_id:, start_date: nil, end_date: nil)
params = {}
params[:startDate] = start_date.to_date.to_s if start_date
params[:endDate] = end_date.to_date.to_s if end_date
get_json("/api/v1/accounts/#{account_id}/activities", params)
end
# Cross-account activities endpoint. Returns Array<Hash>.
def get_activities(start_date: nil, end_date: nil, accounts: nil, brokerage_authorizations: nil, type: nil)View on GitHub (pinned to e69894adb9)
Solutions
- Read the keys in the message - an 'error' key means a masked upstream failure; unfamiliar keys mean the response schema changed
- Keep the previous positions snapshot (that is the guard's purpose) and surface a 'sync failed, showing last known data' state to the user
- Reproduce with curl against /api/v1/accounts/<id>/positions/all using the same auth to see the raw shape
- If the schema legitimately changed, update get_positions to map the new envelope before this guard trips for every account
Example fix
# caller: preserve last good snapshot when the guard trips
begin
positions = item.get_positions(account_id: id)
snapshot.update!(positions: positions)
rescue Provider::Snaptrade::ApiError => e
Rails.logger.warn("positions sync failed, keeping snapshot: #{e.message}")
end Defensive patterns
Strategy: fallback
Type guard
def snaptrade_positions_envelope?(response) response.is_a?(Hash) && response["results"].is_a?(Array) end
Try / catch
begin
positions = snaptrade.get_positions(account_id: account.id)
snapshot.update!(positions: positions)
rescue Provider::Snaptrade::ApiError => e
Rails.logger.warn("Keeping last snapshot for account #{account.id}: #{e.message}")
# do not overwrite with [] - the guard fired because the envelope was wrong
end Prevention
- Never translate this error into 'zero positions' - it exists to protect the last good snapshot
- Log the keys from the message to detect SnapTrade schema drift early
- Add regression VCR cassettes for the positions envelope so renames fail tests first
When it happens
Trigger: SnapTrade returns an error-shaped object (e.g. {"error": ...} with HTTP 200), changes its pagination envelope so positions live under a different key, or returns a truncated/partial payload; the keys in the message reveal which shape arrived.
Common situations: SnapTrade ships an API version that renames the results field; a gateway returns a 200 with an error body during partial outage; instrument filtering assumptions break after the provider adds a new top-level field and drops another.
Related errors
- invalid_response
- pagination_error
- request_failed
- SnapTrade OAuth token request failed: #{error}
- Network error after #{max_retries} retries: #{e.message}
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/1705e77f93de5c4c.
Report an issue: GitHub.