we-promise/sure · warning · Provider::Tiingo::Error
Unexpected response format from search endpoint
Error message
Unexpected response format from search endpoint
What it means
search_securities GETs /tiingo/utilities/search and expects a JSON array of match objects. After JSON.parse and check_api_error! (which only handles Hash payloads containing a 'detail' key), any non-Array payload raises Error 'Unexpected response format from search endpoint'. So Tiingo returned valid JSON that is neither a result list nor a detail-bearing error object -- typically an upstream format change, a maintenance/error object without 'detail', or a proxy/WAF interstitial returning JSON.
Source
Thrown at app/models/provider/tiingo.rb:83
end
# ================================
# Securities
# ================================
def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
with_provider_response do
throttle_request
response = client.get("#{base_url}/tiingo/utilities/search") do |req|
req.params["query"] = symbol
end
parsed = JSON.parse(response.body)
check_api_error!(parsed)
unless parsed.is_a?(Array)
raise Error, "Unexpected response format from search endpoint"
end
# Tiingo's daily-price endpoints are looked up by ticker alone, so every
# result sharing a ticker resolves to the same priced entry (see
# best_match_for_ticker) and therefore the same currency. Resolve it once
# per unique ticker and reuse it both for caching (so fetch_security_prices
# can use it without a second search request) and for the Security objects
# below, so what's shown in search results always matches what
# fetch_security_prices will later return.
matches_by_ticker = parsed.filter_map { |security| security["ticker"] }.map(&:upcase).uniq.index_with do |ticker|
best_match_for_ticker(parsed, ticker)
end
currency_by_ticker = matches_by_ticker.transform_values { |match| currency_for_country(match&.dig("countryCode")) }
currency_by_ticker.each do |ticker, currency|
next if currency.blank?
View on GitHub (pinned to e69894adb9)
Solutions
- Inspect the raw body: reproduce in console with client.get and log response.body to see the actual payload shape
- If the payload is an error object, extend check_api_error! to recognize that key (as was done for 'detail')
- Report/check Tiingo API changelog if the search endpoint legitimately changed shape; treat as transient and retry once before surfacing
Example fix
# before
parsed = JSON.parse(response.body)
check_api_error!(parsed)
raise Error, 'Unexpected response format from search endpoint' unless parsed.is_a?(Array)
# after (capture the payload for diagnosis)
parsed = JSON.parse(response.body)
check_api_error!(parsed)
unless parsed.is_a?(Array)
Rails.logger.warn("Tiingo search non-array response: #{parsed.inspect}")
raise Error, "Unexpected response format from search endpoint: #{parsed.inspect}"[0, 200]
end Defensive patterns
Strategy: try-catch
Type guard
def tiingo_search_payload?(parsed) parsed.is_a?(Array) end
Try / catch
begin
results = provider.search_securities('AAPL')
rescue Provider::Tiingo::Error => e
Rails.logger.warn("Tiingo search unavailable: #{e.message}")
results = Provider::Response.new(data: [], error: nil)
end Prevention
- Log raw response bodies for non-array payloads so format changes are diagnosed from the first occurrence
- Keep search-dependent flows degradable (empty results) instead of fatal
When it happens
Trigger: Calling search_securities(symbol) when Tiingo's search endpoint responds with a JSON object instead of a list -- e.g. an undocumented error shape like {'Error': '...'}, an account-level message, or an API version change. Distinguished from API errors by check_api_error! having already passed (no 'detail' key present).
Common situations: Tiingo shipping a response-shape change; symbol query hitting an edge case (empty/odd characters) that returns an object; intermediary (corporate proxy, Cloudflare) injecting a JSON block page; API key tier mismatch returning an object without 'detail'.
Related errors
- API error: #{error_message}
- Could not determine currency for #{symbol} from Tiingo searc
- No data returned from search endpoint
- Could not save that passkey or security key. Please try agai
- parse_error
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/c6d8bcb98a009ca0.
Report an issue: GitHub.