{"record":{"id":"89753ea23709bb4c","repo":"we-promise/sure","slug":"api-error-parsed-error","errorCode":null,"errorMessage":"API error: #{parsed[\"error\"]}","messagePattern":"API error: #(.+?)","errorType":"exception","errorClass":"Provider::Eodhd::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/eodhd.rb","lineNumber":304,"sourceCode":"    # Uses atomic increment-then-check to avoid TOCTOU races between concurrent workers.\n    def enforce_daily_limit!\n      new_count = Rails.cache.increment(daily_cache_key, 1, expires_in: 24.hours).to_i\n\n      if new_count > max_requests_per_day\n        raise RateLimitError, \"EODHD daily rate limit of #{max_requests_per_day} requests exhausted\"\n      end\n    end\n\n    # throttle_request and min_request_interval provided by RateLimitable\n\n    def max_requests_per_day\n      ENV.fetch(\"EODHD_MAX_REQUESTS_PER_DAY\", MAX_REQUESTS_PER_DAY).to_i\n    end\n\n    def check_api_error!(parsed)\n      return unless parsed.is_a?(Hash) && parsed[\"error\"].present?\n\n      raise Error, \"API error: #{parsed[\"error\"]}\"\n    end\nend\n","sourceCodeStart":286,"sourceCodeEnd":307,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/eodhd.rb#L286-L307","documentation":"Raised as Provider::Eodhd::Error when a parsed EODHD response is a Hash containing a present 'error' key. EODHD signals failures in-band: HTTP status is often 200 while the JSON body carries {\"error\": \"...\"}. check_api_error! runs on every parsed response (search, EOD prices, fundamentals) before format validation, converting these in-band errors into a Ruby exception with the upstream message preserved verbatim.","triggerScenarios":"Any EODHD call whose body includes a non-blank 'error' field: invalid or expired api_token ('Invalid API key'), unknown symbol ('Not found'), exceeding the account's request quota ('Exceeded the daily requests limit'), or restricted access to a data feed the token does not include. Triggered by the same GETs as search_securities and fetch_security_prices.","commonSituations":"Expired or typo'd API token after credentials rotation, free-tier token hitting EODHD's own (separate from the local) request limit, requesting US exchanges on a plan without US data access, delisted symbols, and environments sharing one token across many app instances.","solutions":["Read the message text: 'Invalid API key/Token' → fix credentials; 'Exceeded ... limit' → EODHD-side quota (distinct from the local RateLimitError); 'Not found' → bad symbol/exchange suffix","Verify the token against EODHD's user/details endpoint to confirm validity and remaining quota","For quota errors, upgrade the plan or reduce request volume (see the local daily limiter) — retrying will not help","For symbol errors, re-run a search_securities lookup to get the correct ticker/exchange mapping","Store the corrected token in the provider settings UI / credential store rather than ENV snapshots to avoid stale copies"],"exampleFix":"# before: token errors surface as a generic raised string, caller retries pointlessly\nbegin\n  provider.fetch_security_prices(symbol: \"AAPL.US\", start_date: from, end_date: to)\nrescue Provider::Eodhd::Error => e\n  retry_job\nend\n\n# after: branch on the upstream message; only retry non-auth, non-quota failures\nrescue Provider::Eodhd::Error => e\n  raise if e.message.match?(/Invalid API key|Exceeded/i)\n  retry_job if e.message.match?(/temporary|timeout/i)","handlingStrategy":"try-catch","validationCode":"# Validate the token's account status before starting syncs\nuser_info = client.get(\"#{base_url}/api/user-details\") # adjust per your client\nraise if user_info.body.include?('\"error\"') # surface token problems up front","typeGuard":null,"tryCatchPattern":"begin\n  data = provider.fetch_security_prices(...)\nrescue Provider::Eodhd::Error => e\n  case e.message\n  when /Invalid API key|Token/i then disconnect_provider(\"EODHD auth failed: #{e.message}\")\n  when /Exceeded|limit/i         then schedule_retry_tomorrow\n  else raise\n  end\nend","preventionTips":["Store EODHD tokens in the provider settings store and validate them on save (call a cheap endpoint)","Alert on quota-type in-band errors separately from transient ones — retrying quota errors wastes budget","Keep the local daily limiter (see RateLimitError) below EODHD's plan quota to hit your own guard first","Log the exact upstream message via DebugLogEntry for support triage"],"tags":["eodhd","api-error","quota","auth","securities"],"backgroundTag":"api-error-response","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}