{"record":{"id":"f2b0be1bd63286e9","repo":"we-promise/sure","slug":"frankfurter-currencies-endpoint-returned-no-data","errorCode":null,"errorMessage":"Frankfurter currencies endpoint returned no data","messagePattern":"Frankfurter currencies endpoint returned no data","errorType":"exception","errorClass":"Provider::Frankfurter::Error","httpStatus":null,"severity":"warning","filePath":"app/models/provider/frankfurter.rb","lineNumber":31,"sourceCode":"# unlike v1 this provider does not need its own lookback-window logic.\nclass Provider::Frankfurter < Provider\n  include ExchangeRateConcept, RateLimitable\n  extend SslConfigurable\n\n  Error = Class.new(Provider::Error)\n  RateLimitError = Class.new(Error)\n\n  # No published rate limit, but a light throttle is cheap insurance.\n  MIN_REQUEST_INTERVAL = 0.15\n\n  def initialize\n    # No API key required, public endpoint only.\n  end\n\n  def healthy?\n    with_provider_response do\n      body = get_json(\"/currencies\")\n      raise Error, \"Frankfurter currencies endpoint returned no data\" if body.blank?\n      true\n    end\n  end\n\n  def usage\n    with_provider_response do\n      UsageData.new(used: nil, limit: nil, utilization: nil, plan: \"Free (no key required)\")\n    end\n  end\n\n  # GET /rate/{base}/{quote}?date=... -> { date:, base:, quote:, rate: }.\n  # Frankfurter carries forward weekends/holidays itself, so the returned\n  # date may differ from the requested one but is never simply missing.\n  def fetch_exchange_rate(from:, to:, date:)\n    from = sanitize_currency(from)\n    to = sanitize_currency(to)\n\n    with_provider_response do","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/frankfurter.rb#L13-L49","documentation":"Raised as Provider::Frankfurter::Error by healthy? when GET /currencies on the Frankfurter service returns a blank body (empty string, nil, empty JSON). Frankfurter is a keyless public FX API; this check is the provider's health probe. A blank body means the service responded with nothing (or JSON.parse produced ''/nil), so the provider treats the integration as unhealthy rather than reporting a false-positive OK.","triggerScenarios":"Calling healthy? (e.g. from a provider status page or connection test) when the Frankfurter deployment returns an empty 200 body, a proxy strips the body, or an FRANKFURTER_URL override points at a mirror that 200s with no content. Note: non-2xx statuses raise Faraday errors instead; this error is specifically about a successful-but-empty response.","commonSituations":"Health checks against a self-hosted Frankfurter instance that is still booting, a misconfigured FRANKFURTER_URL pointing at the wrong path/host, CDN or ingress returning empty bodies under load, and frankfurter.dev API changes to the /currencies route.","solutions":["curl the endpoint your app is configured with: curl -i \"${FRANKFURTER_URL:-https://api.frankfurter.dev/v2}/currencies\" and confirm a non-empty JSON body","Check FRANKFURTER_URL in the environment — a wrong base path (missing /v2 or trailing path) yields empty responses","If self-hosting Frankfurter, verify the service is fully started and its upstream ECB data fetch has run","Retry the health check after a short delay; transient empty responses from load balancers usually clear","If api.frankfurter.dev itself is returning empty bodies, check the project's status page — nothing client-side to fix"],"exampleFix":"# before: healthy? raises, status page shows provider down on a transient empty body\nprovider.healthy?\n\n# after: treat one blank probe as degraded, not failed\nbegin\n  provider.healthy?\nrescue Provider::Frankfurter::Error => e\n  Rails.logger.warn(\"Frankfurter health probe failed once: #{e.message}; retrying\")\n  sleep 2\n  provider.healthy?\nend","handlingStrategy":"retry","validationCode":"# Verify the configured URL returns a JSON object before relying on healthy?\nuri = URI(\"#{ENV['FRANKFURTER_URL'].presence || 'https://api.frankfurter.dev/v2'}/currencies\")\nbody = Net::HTTP.get(uri)\nJSON.parse(body).is_a?(Hash) && body.length > 2","typeGuard":null,"tryCatchPattern":"def frankfurter_healthy?(attempts: 2)\n  attempts.times do\n    return true if provider.healthy?\n  rescue Provider::Frankfurter::Error => e\n    Rails.logger.warn(\"Frankfurter probe failed: #{e.message}\")\n  end\n  false\nend","preventionTips":["Treat a single failed health probe as 'degraded' — retry before declaring the provider down","Pin FRANKFURTER_URL to the official versioned host in all environments","Health-check third-party providers from the same network path the sync jobs use","Cache the last-known health result briefly to avoid hammering /currencies from every process"],"tags":["frankfurter","health-check","empty-response","fx-rates","public-api"],"backgroundTag":"empty-api-response","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}