{"record":{"id":"af16089455aa6cf4","repo":"we-promise/sure","slug":"api-error-error-message","errorCode":null,"errorMessage":"API error: #{error_message}","messagePattern":"API error: #(.+?)","errorType":"exception","errorClass":"Provider::Tiingo::InvalidSecurityPriceError","httpStatus":null,"severity":"error","filePath":"app/models/provider/tiingo.rb","lineNumber":184,"sourceCode":"    end\n  end\n\n  def fetch_security_prices(symbol:, exchange_operating_mic: nil, start_date:, end_date:)\n    with_provider_response do\n      throttle_request\n      track_symbol(symbol)\n\n      response = client.get(\"#{base_url}/tiingo/daily/#{CGI.escape(symbol)}/prices\") do |req|\n        req.params[\"startDate\"] = start_date.to_s\n        req.params[\"endDate\"] = end_date.to_s\n      end\n\n      parsed = JSON.parse(response.body)\n      check_api_error!(parsed)\n\n      unless parsed.is_a?(Array)\n        error_message = parsed.is_a?(Hash) ? (parsed[\"detail\"] || \"Unexpected response format\") : \"Unexpected response format\"\n        raise InvalidSecurityPriceError, \"API error: #{error_message}\"\n      end\n\n      # Prefer cached currency from search results to avoid a second API call\n      cache_key = \"tiingo:currency:#{symbol.upcase}\"\n      currency = Rails.cache.read(cache_key) || fetch_currency_for_symbol(symbol)\n\n      parsed.map do |resp|\n        price = resp[\"close\"]\n        date = resp[\"date\"]\n\n        if price.nil? || price.to_f <= 0\n          Rails.logger.warn(\"#{self.class.name} returned invalid price data for security #{symbol} on: #{date}.  Price data: #{price.inspect}\")\n          next\n        end\n\n        Price.new(\n          symbol: symbol,\n          date: Date.parse(date),","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/tiingo.rb#L166-L202","documentation":"fetch_security_prices GETs /tiingo/daily/<symbol>/prices and expects a JSON array of candles. check_api_error! runs first and only raises for Hash payloads with a 'detail' key, so reaching the unless Array branch means the payload is a Hash WITHOUT 'detail' -- the code then reads parsed['detail'] (nil here) and falls back to error_message 'Unexpected response format', raising InvalidSecurityPriceError 'API error: Unexpected response format'. This is Tiingo returning an object-shaped error this client does not model, most notably the 404-style response for a symbol unknown to the daily endpoint.","triggerScenarios":"Calling fetch_security_prices with a ticker that has no Tiingo daily series (misspelled symbol, delisted, mutual fund/forex/crypto ticker on the equities endpoint, or an exchange-suffixed form the provider doesn't strip); or any Tiingo error object lacking the 'detail' field (quota/account notices phrased differently).","commonSituations":"User-typed or imported symbols never validated against search_securities first; symbols with exchange suffixes (.TO, .L) passed verbatim; upstream A/B changes to Tiingo's error payload; stale symbols after ticker changes/renames.","solutions":["Verify the symbol resolves on Tiingo: run search_securities(symbol) first and use the returned normalized ticker","Log the raw response body to identify which key Tiingo used for the error, then extend check_api_error! to map it","For non-equity asset classes, use the appropriate Tiingo endpoint/provider instead of /tiingo/daily"],"exampleFix":"# before\nprices = provider.fetch_security_prices(symbol: raw_symbol, start_date: from, end_date: to)\n\n# after\nmatches = provider.search_securities(raw_symbol)\nbest = matches.data.find { |s| s.symbol == raw_symbol.upcase } or raise UnknownSecurityError, raw_symbol\nprices = provider.fetch_security_prices(symbol: best.symbol, exchange_operating_mic: best.exchange_operating_mic, start_date: from, end_date: to)","handlingStrategy":"validation","validationCode":"# Resolve/normalize the symbol through search before pricing\nmatch = provider.search_securities(raw).data.first or raise UnknownSymbolError, raw\nsymbol = match.symbol","typeGuard":"def tiingo_daily_symbol?(search_result)\n  search_result.respond_to?(:symbol) && search_result.exchange_operating_mic.present?\nend","tryCatchPattern":"begin\n  prices = provider.fetch_security_prices(symbol:, start_date:, end_date:)\nrescue Provider::Tiingo::InvalidSecurityPriceError => e\n  Rails.logger.warn(\"Tiingo price lookup failed for #{symbol}: #{e.message}\")\n  prices = Provider::Response.new(data: [], error: nil)\nend","preventionTips":["Always route user-entered symbols through search_securities before the prices endpoint","Strip exchange suffixes or map them to Tiingo's expected ticker form"],"tags":["tiingo","price-data","api-response","unknown-symbol","json"],"backgroundTag":"unexpected-api-response-format","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}