{"record":{"id":"2ea7c16cd6e28bd7","repo":"we-promise/sure","slug":"api-error-detail","errorCode":null,"errorMessage":"API error: #{detail}","messagePattern":"API error: #(.+?)","errorType":"exception","errorClass":"Provider::Tiingo::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/tiingo.rb","lineNumber":342,"sourceCode":"    # when a US entry exists for the ticker, that's the one actually backing\n    # the price data. Fall back to the first match otherwise.\n    def best_match_for_ticker(results, ticker)\n      return nil if ticker.blank?\n\n      matches = results.select { |s| s[\"ticker\"]&.upcase == ticker.upcase }\n      matches.find { |s| s[\"countryCode\"] == \"US\" } || matches.first\n    end\n\n    def check_api_error!(parsed)\n      return unless parsed.is_a?(Hash) && parsed[\"detail\"].present?\n\n      detail = parsed[\"detail\"]\n\n      if detail.downcase.include?(\"rate limit\") || detail.downcase.include?(\"too many\")\n        raise RateLimitError, detail\n      end\n\n      raise Error, \"API error: #{detail}\"\n    end\nend\n","sourceCodeStart":324,"sourceCodeEnd":345,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/tiingo.rb#L324-L345","documentation":"The sibling branch of check_api_error!: the response is a Hash with a non-empty 'detail' string that does NOT mention rate limits, so it is raised verbatim as Error 'API error: <detail>'. Typical Tiingo details include 'Api token invalid.', 'Forbidden', 'Not Found' (unknown symbol for that endpoint), and account messages. This is Tiingo rejecting the request with a human-readable reason; the code preserves it exactly.","triggerScenarios":"Calling any Tiingo endpoint with a bad/empty/expired api key ('Api token invalid'), a ticker that doesn't exist on that endpoint ('Not Found'), insufficient plan access ('Forbidden'), or an account-level notice. The constructor Provider::Tiingo.new(api_key) does no up-front validation, so the first API call surfaces it.","commonSituations":"Wrong or placeholder TIINGO_API_KEY in env; key revoked after plan expiry; symbol typo or delisted ticker; endpoint not included in the current plan; trailing whitespace/newline pasted into the key.","solutions":["Read the detail: 'Api token invalid' -> fix/renew the key; 'Not Found' -> verify the symbol via search_securities; 'Forbidden' -> check plan entitlements","Verify connectivity at setup with healthy? (GET /tiingo/daily/AAPL) in a console for that key","Trim whitespace on the stored key and confirm it's set in the environment the job/web process actually uses"],"exampleFix":"# before\nprovider = Provider::Tiingo.new(ENV['TIINGO_API_KEY'])\nprovider.fetch_security_prices(...)\n\n# after (fail fast at configuration time)\nprovider = Provider::Tiingo.new(ENV['TIINGO_API_KEY'].to_s.strip)\nraise Provider::Tiingo::Error, 'Tiingo API key missing' if provider.respond_to?(:healthy?) && !provider.healthy?\nprovider.fetch_security_prices(...)","handlingStrategy":"try-catch","validationCode":"# Validate the key and endpoint at configuration time\nraise 'TIINGO_API_KEY missing' if ENV['TIINGO_API_KEY'].blank?","typeGuard":null,"tryCatchPattern":"begin\n  provider.fetch_security_prices(...)\nrescue Provider::Tiingo::Error => e\n  raise if e.message.include?('rate limit')\n  provider.mark_unhealthy(reason: e.message) # 'Api token invalid', 'Not Found', etc.\nend","preventionTips":["Strip whitespace from the API key at construction","Run healthy? after key changes to catch invalid tokens at setup rather than mid-sync"],"tags":["tiingo","api-key","authentication","api-error","configuration"],"backgroundTag":"invalid-api-key","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}