{"record":{"id":"514351020941f6d6","repo":"we-promise/sure","slug":"unexpected-response-format-from-search-api","errorCode":null,"errorMessage":"Unexpected response format from search API","messagePattern":"Unexpected response format from search API","errorType":"exception","errorClass":"Provider::Eodhd::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/eodhd.rb","lineNumber":131,"sourceCode":"  # ================================\n  #           Securities\n  # ================================\n\n  def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)\n    with_provider_response do\n      enforce_daily_limit!\n      throttle_request\n\n      response = client.get(\"#{base_url}/api/search/#{CGI.escape(symbol)}\") do |req|\n        req.params[\"api_token\"] = api_key\n        req.params[\"fmt\"] = \"json\"\n      end\n\n      parsed = JSON.parse(response.body)\n      check_api_error!(parsed)\n\n      unless parsed.is_a?(Array)\n        raise Error, \"Unexpected response format from search API\"\n      end\n\n      parsed.first(25).map do |security|\n        eodhd_exchange = security.dig(\"Exchange\")\n        mic = EODHD_EXCHANGE_TO_MIC[eodhd_exchange] || eodhd_exchange\n        country = EODHD_COUNTRY_TO_CODE[security.dig(\"Country\")]\n        code = security.dig(\"Code\")\n        currency = security.dig(\"Currency\")\n\n        # Cache the API-returned currency so fetch_security_prices can use it\n        if currency.present? && mic.present?\n          cache_key = \"eodhd:currency:#{code.upcase}:#{mic}\"\n          Rails.cache.write(cache_key, currency, expires_in: 24.hours)\n        end\n\n        Security.new(\n          symbol: code,\n          name: security.dig(\"Name\"),","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/eodhd.rb#L113-L149","documentation":"Raised as Provider::Eodhd::Error when the EODHD search endpoint ('GET /api/search/<symbol>') returns a JSON body that is not a top-level Array. EODHD's search API contract is a bare JSON array of security objects; a Hash body means the API changed shape, returned an error envelope that check_api_error! did not recognize (its 'error' key was absent/blank), or an HTML/XML error page was parsed into an unexpected structure. This is a hard schema violation — the code refuses to guess and raises instead of returning partial data.","triggerScenarios":"Calling search_securities (symbol lookup / security search flow) with a valid API token where the response body parses as JSON but is an object (e.g. {\"message\": \"...\"}) rather than an array. Happens when EODHD changes its API version, returns an undocumented error envelope without an 'error' key, or a gateway (Cloudflare) interposes a JSON error object.","commonSituations":"EODHD deploying a breaking API change, free-tier accounts hitting an undocumented quota response, stale base URL ENV override pointing at an old API version, or intermediaries rewriting responses. Typically surfaces suddenly across all symbol searches for every EODHD user of the app.","solutions":["Reproduce manually: curl 'https://eodhd.com/api/search/AAPL?api_token=YOUR_TOKEN&fmt=json' and inspect the raw body shape","Check the EODHD API changelog/status page for announced response-format changes","If the body is an error envelope without an 'error' key, extend check_api_error! to recognize that key and raise a precise API error instead","Verify the base_url ENV override (if any) points at the current EODHD API version","Report the format change upstream if the body is valid but structurally different, and pin to the documented v1 shape until fixed"],"exampleFix":"# before: only a Hash with 'error' is treated as an API error\nparsed = JSON.parse(response.body)\ncheck_api_error!(parsed)\nraise Error, \"Unexpected response format from search API\" unless parsed.is_a?(Array)\n\n# after: surface the actual body so the failure is diagnosable\nraise Error, \"Unexpected response format from search API: #{parsed.class} #{parsed.to_s.truncate(200)}\" unless parsed.is_a?(Array)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"# Ruby shape guard for EODHD search responses\nmodule EodhdSearchGuard\n  def self.valid?(parsed)\n    parsed.is_a?(Array) && parsed.all? { |s| s.is_a?(Hash) && s[\"Code\"].present? }\n  end\nend\n\nparsed = JSON.parse(raw_body)\nreturn fallback_search(parsed) if EodhdSearchGuard.valid?(parsed)","tryCatchPattern":"begin\n  securities = provider.search_securities(query)\nrescue Provider::Eodhd::Error => e\n  if e.message.include?(\"Unexpected response format\")\n    Rails.logger.error(\"EODHD search shape changed: #{e.message}\")\n    [] # degrade gracefully — user sees empty results, app stays up\n  else\n    raise\n  end\nend","preventionTips":["Pin the EODHD base URL to a specific API version and review changelogs before upgrading","Log raw (truncated) bodies on shape violations so contract drift is diagnosed in minutes","Wrap search flows so a malformed response degrades to empty results instead of a 500","Contract-test the live API shape in a staging ping job to catch drift before users"],"tags":["eodhd","api-contract","schema-validation","json","securities"],"backgroundTag":"schema-validation-failed","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}