we-promise/sure · error · Provider::Tiingo::Error

API error: #{detail}

Error message

API error: #{detail}

What it means

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.

Source

Thrown at app/models/provider/tiingo.rb:342

    # when a US entry exists for the ticker, that's the one actually backing
    # the price data. Fall back to the first match otherwise.
    def best_match_for_ticker(results, ticker)
      return nil if ticker.blank?

      matches = results.select { |s| s["ticker"]&.upcase == ticker.upcase }
      matches.find { |s| s["countryCode"] == "US" } || matches.first
    end

    def check_api_error!(parsed)
      return unless parsed.is_a?(Hash) && parsed["detail"].present?

      detail = parsed["detail"]

      if detail.downcase.include?("rate limit") || detail.downcase.include?("too many")
        raise RateLimitError, detail
      end

      raise Error, "API error: #{detail}"
    end
end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Read the detail: 'Api token invalid' -> fix/renew the key; 'Not Found' -> verify the symbol via search_securities; 'Forbidden' -> check plan entitlements
  2. Verify connectivity at setup with healthy? (GET /tiingo/daily/AAPL) in a console for that key
  3. Trim whitespace on the stored key and confirm it's set in the environment the job/web process actually uses

Example fix

# before
provider = Provider::Tiingo.new(ENV['TIINGO_API_KEY'])
provider.fetch_security_prices(...)

# after (fail fast at configuration time)
provider = Provider::Tiingo.new(ENV['TIINGO_API_KEY'].to_s.strip)
raise Provider::Tiingo::Error, 'Tiingo API key missing' if provider.respond_to?(:healthy?) && !provider.healthy?
provider.fetch_security_prices(...)
Defensive patterns

Strategy: try-catch

Validate before calling

# Validate the key and endpoint at configuration time
raise 'TIINGO_API_KEY missing' if ENV['TIINGO_API_KEY'].blank?

Try / catch

begin
  provider.fetch_security_prices(...)
rescue Provider::Tiingo::Error => e
  raise if e.message.include?('rate limit')
  provider.mark_unhealthy(reason: e.message) # 'Api token invalid', 'Not Found', etc.
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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