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

  1. Inspect the raw body: reproduce in console with client.get and log response.body to see the actual payload shape
  2. If the payload is an error object, extend check_api_error! to recognize that key (as was done for 'detail')
  3. 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

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


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