{"record":{"id":"0daee37b781368d2","repo":"we-promise/sure","slug":"unexpected-response-format-from-eod-api","errorCode":null,"errorMessage":"Unexpected response format from EOD API","messagePattern":"Unexpected response format from EOD API","errorType":"exception","errorClass":"Provider::Eodhd::InvalidSecurityPriceError","httpStatus":null,"severity":"error","filePath":"app/models/provider/eodhd.rb","lineNumber":217,"sourceCode":"  def fetch_security_prices(symbol:, exchange_operating_mic: nil, start_date:, end_date:)\n    with_provider_response do\n      enforce_daily_limit!\n      throttle_request\n\n      ticker = eodhd_symbol(symbol, exchange_operating_mic)\n\n      response = client.get(\"#{base_url}/api/eod/#{CGI.escape(ticker)}\") do |req|\n        req.params[\"api_token\"] = api_key\n        req.params[\"fmt\"] = \"json\"\n        req.params[\"from\"] = start_date.to_s\n        req.params[\"to\"] = 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        raise InvalidSecurityPriceError, \"Unexpected response format from EOD API\"\n      end\n\n      # Prefer cached currency from search results; fall back to hardcoded map\n      cache_key = \"eodhd:currency:#{symbol.upcase}:#{exchange_operating_mic}\"\n      eodhd_exchange = MIC_TO_EODHD_EXCHANGE[exchange_operating_mic]\n      currency = Rails.cache.read(cache_key) || EXCHANGE_CURRENCY[eodhd_exchange]\n\n      parsed.map do |resp|\n        price = resp.dig(\"close\")\n        date = resp.dig(\"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,","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/eodhd.rb#L199-L235","documentation":"Raised as Provider::Eodhd::InvalidSecurityPriceError when the EODHD end-of-day endpoint ('GET /api/eod/<ticker>') returns a JSON body that is not a top-level Array. The EOD contract is a bare array of {date, open, high, low, close, volume} rows; a non-array body means the API returned an unrecognized envelope (no 'error' key, so check_api_error! passed) or the response was rewritten by a gateway. The guard fires before any row mapping, so no partial prices are returned.","triggerScenarios":"Calling fetch_security_prices / fetch_security_price when the parsed body is a Hash or String instead of an Array — e.g. EODHD changes response format, an auth/error envelope is returned without an 'error' key, or the api_token is invalid in a way that yields a JSON object message rather than the standard error shape.","commonSituations":"EODHD API version changes breaking the v1 array contract, expired or suspended API tokens producing non-standard error bodies, third-party base_url mirrors with different envelopes, and gateway-injected JSON error objects. Affects every price fetch app-wide when it happens.","solutions":["curl the endpoint with the same params and token to see the raw body: curl 'https://eodhd.com/api/eod/AAPL.US?api_token=TOKEN&fmt=json&from=2025-01-01&to=2025-01-02'","If the body is an error envelope with a different key, teach check_api_error! to detect that key so users get a real API error message","Validate the api_token against the user/details endpoint to rule out auth issues","Check EODHD status/changelog for response-format changes; pin or adapt the parser accordingly","Include a truncated body sample in the raised message to speed up diagnosis"],"exampleFix":"# before: opaque class-level failure\nraise InvalidSecurityPriceError, \"Unexpected response format from EOD API\" unless parsed.is_a?(Array)\n\n# after: include what was actually received\nunless parsed.is_a?(Array)\n  raise InvalidSecurityPriceError, \"Unexpected response format from EOD API (got #{parsed.class}): #{parsed.to_s.truncate(200)}\"\nend","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"# Ruby shape guard for EODHD EOD rows\ndef valid_eod_payload?(parsed)\n  parsed.is_a?(Array) && parsed.all? do |row|\n    row.is_a?(Hash) && row[\"date\"].present? && row[\"close\"].present?\n  end\nend","tryCatchPattern":"begin\n  prices = provider.fetch_security_prices(symbol: sym, start_date: from, end_date: to)\nrescue Provider::Eodhd::InvalidSecurityPriceError => e\n  if e.message.include?(\"Unexpected response format\")\n    notify_ops(\"EODHD EOD response shape drift: #{e.message}\")\n  end\n  raise\nend","preventionTips":["Distinguish shape errors ('Unexpected response format') from empty-data errors in handling and alerting","Include a body excerpt in raised shape errors to make contract drift diagnosable from logs","Contract-test the EOD endpoint shape on a staging schedule","Keep the base URL pinned; verify after any EODHD account/plan migration"],"tags":["eodhd","api-contract","schema-validation","json","historical-prices"],"backgroundTag":"schema-validation-failed","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}