we-promise/sure · error · Provider::Mfapi::InvalidSecurityPriceError

No NAV data returned for scheme #

Error message

No NAV data returned for scheme #{symbol}

What it means

Raised by Provider::Mfapi#fetch_security_prices (as InvalidSecurityPriceError) when the /mf/{schemeCode}?startDate=...&endDate=... response is valid JSON but its data field is either absent (nil) or not an Array. The MFAPI NAV contract is {meta: {...}, data: [{date, nav}, ...]}, so a missing/non-array data means the payload is an error or maintenance envelope that check_api_error! did not classify (it only fires on status ERROR/FAIL). Distinct from error 341: here the payload shape itself is wrong, before any row filtering happens.

Solutions

  1. Capture and inspect response.body for the failing schemeCode to see which envelope MFAPI actually returned.
  2. Treat a missing data key as an upstream error: log it via DebugLogEntry with provider_key and retry once after backoff before raising.
  3. Confirm the schemeCode exists via the /mf/search endpoint first when the symbol is user-supplied.
  4. If MFAPI renamed the field, map the new key with parsed["data"] || parsed["nav"] fallback only after verifying upstream docs.

Example fix

# before
nav_data = parsed["data"]
if nav_data.nil? || !nav_data.is_a?(Array)
  raise InvalidSecurityPriceError, "No NAV data returned for scheme #{symbol}"
end

# after
nav_data = parsed.is_a?(Hash) ? parsed["data"] : nil
if nav_data.nil? || !nav_data.is_a?(Array)
  DebugLogEntry.capture("mfapi", :error, "Unexpected NAV payload for #{symbol}", metadata: { body: parsed.to_s.truncate(500) })
  raise InvalidSecurityPriceError, "No NAV data returned for scheme #{symbol}"
end
Defensive patterns

Strategy: try-catch

Try / catch

begin
  series = provider.fetch_security_prices(symbol: code, start_date: from, end_date: to)
rescue Provider::Mfapi::InvalidSecurityPriceError => e
  DebugLogEntry.capture("mfapi", :error, e.message, provider_key: "mfapi")
  series = ProviderResponse.new(data: [])
end

Prevention

When it happens

Trigger: MFAPI returns {"message": "..."} or {} for an overloaded/rate-limited request; a scheme that exists but returns an object without data while its status field is missing; upstream schema change renaming data; intermediate proxy returning valid-JSON error object without status.

Common situations: Hitting MFAPI's unwritten rate limits during a large portfolio backfill; MFAPI maintenance windows; using a schemeCode that triggers a non-standard response (e.g. non-numeric garbage in the URL segment, though that usually yields status ERROR); CDN edge responses.

Related errors


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

Appendix: source

Thrown at app/models/provider/mfapi.rb:114

      historical_data.data.select { |p| p.date <= date }.max_by(&:date) || historical_data.data.first
    end
  end

  def fetch_security_prices(symbol:, exchange_operating_mic: nil, start_date:, end_date:)
    with_provider_response do
      throttle_request
      response = client.get("#{base_url}/mf/#{CGI.escape(symbol)}") do |req|
        req.params["startDate"] = start_date.to_s
        req.params["endDate"] = end_date.to_s
      end

      parsed = JSON.parse(response.body)
      check_api_error!(parsed)

      nav_data = parsed["data"]

      if nav_data.nil? || !nav_data.is_a?(Array)
        raise InvalidSecurityPriceError, "No NAV data returned for scheme #{symbol}"
      end

      nav_data.filter_map do |entry|
        nav = entry["nav"]
        date_str = entry["date"]

        next if nav.nil? || nav.to_f <= 0 || date_str.blank?

        # MFAPI returns dates as DD-MM-YYYY
        date = Date.strptime(date_str, "%d-%m-%Y")

        Price.new(
          symbol: symbol,
          date: date,
          price: nav.to_f,
          currency: "INR",
          exchange_operating_mic: exchange_operating_mic
        )

View on GitHub (pinned to e69894adb9)