{"record":{"id":"127920b724910c57","repo":"we-promise/sure","slug":"unexpected-response-format-from-search-endpoint","errorCode":null,"errorMessage":"Unexpected response format from search endpoint","messagePattern":"Unexpected response format from search endpoint","errorType":"exception","errorClass":"Provider::Mfapi::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/mfapi.rb","lineNumber":50,"sourceCode":"    end\n  end\n\n  # ================================\n  #           Securities\n  # ================================\n\n  def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)\n    with_provider_response do\n      throttle_request\n      response = client.get(\"#{base_url}/mf/search\") do |req|\n        req.params[\"q\"] = symbol\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 endpoint\"\n      end\n\n      parsed.first(25).map do |fund|\n        Security.new(\n          symbol: fund[\"schemeCode\"].to_s,\n          name: fund[\"schemeName\"],\n          logo_url: nil,\n          exchange_operating_mic: \"XBOM\",\n          country_code: \"IN\",\n          currency: \"INR\"\n        )\n      end\n    end\n  end\n\n  def fetch_security_info(symbol:, exchange_operating_mic:)\n    with_provider_response do\n      throttle_request","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/mfapi.rb#L32-L68","documentation":"Raised by Provider::Mfapi#search_securities when the GET /mf/search?q= response parses as valid JSON but is not a top-level JSON array. MFAPI's search contract is a bare array of fund objects ({schemeCode, schemeName}), so a Hash (e.g. an error envelope without a status field), String, or number means the payload is unusable. check_api_error! only inspects Hash payloads with status ERROR/FAIL, so object-shaped surprises fall through to this guard. It is a hard contract violation against the upstream API shape, not a network or auth failure.","triggerScenarios":"Calling search_securities(\"axis blue\") and MFAPI returning a JSON object such as {\"message\": \"...\"} or an empty object {} instead of an array; an upstream API revision that wraps results in {\"data\": [...]}; a rate-limit or maintenance payload that is valid JSON but object-shaped and lacks status == ERROR/FAIL.","commonSituations":"MFAPI occasionally returns object-shaped maintenance/rate-limit payloads that the status-based check_api_error! does not recognize; a proxy or CDN injecting a JSON error object; upstream schema drift after an MFAPI release; querying with an empty q param that yields a non-array body.","solutions":["Log response.body (or capture it in DebugLogEntry) at the failure site to see the actual payload MFAPI returned.","Broaden check_api_error! to raise the body's message/inspect form for any Hash that is not an Array, so unexpected envelopes surface as API errors with their content.","If upstream now wraps results (e.g. parsed[\"data\"]), map the wrapper: treat a Hash with an Array under a known key as the result list before raising.","Retry once after a short backoff in case the object payload was a transient rate-limit response."],"exampleFix":"# before\nunless parsed.is_a?(Array)\n  raise Error, \"Unexpected response format from search endpoint\"\nend\n\n# after\nresults =\n  if parsed.is_a?(Array)\n    parsed\n  elsif parsed.is_a?(Hash) && parsed[\"data\"].is_a?(Array)\n    Rails.logger.warn(\"MFAPI search returned wrapped payload: #{parsed.keys}\")\n    parsed[\"data\"]\n  else\n    raise Error, \"Unexpected response format from search endpoint: #{parsed.to_s.truncate(200)}\"\n  end","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  result = provider.search_securities(query)\nrescue Provider::Mfapi::Error => e\n  Rails.logger.warn(\"MFAPI search unusable: #{e.message}\")\n  result = nil # show user 'no results' instead of crashing\nend","preventionTips":["Wrap provider search calls and degrade to an empty result set rather than surfacing the raw error in UI.","Capture the raw response body via DebugLogEntry when the shape guard trips so envelope drift is diagnosable later.","Keep a contract test asserting MFAPI /mf/search returns a JSON array, so upstream drift is caught in CI."],"tags":["mfapi","mutual-funds","json","api-contract","response-shape"],"backgroundTag":"unexpected-api-response-format","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}