we-promise/sure · error · Provider::Frankfurter::Error
Unexpected Frankfurter response shape
Error message
Unexpected Frankfurter response shape
What it means
Raised as Provider::Frankfurter::Error when GET /rate/<from>/<to>?date=... returns a body that is not a Hash or has no truthy 'rate' key. The provider expects Frankfurter v2's single-rate shape {date, base, quote, rate}; anything else (array, string, hash without rate) is a contract violation. This is the single-rate path used by fetch_exchange_rate; the same base currency==quote currency shortcut bypasses the network entirely.
Source
Thrown at app/models/provider/frankfurter.rb:54
def usage
with_provider_response do
UsageData.new(used: nil, limit: nil, utilization: nil, plan: "Free (no key required)")
end
end
# GET /rate/{base}/{quote}?date=... -> { date:, base:, quote:, rate: }.
# Frankfurter carries forward weekends/holidays itself, so the returned
# date may differ from the requested one but is never simply missing.
def fetch_exchange_rate(from:, to:, date:)
from = sanitize_currency(from)
to = sanitize_currency(to)
with_provider_response do
if from == to
Rate.new(date: date, from: from, to: to, rate: 1.0)
else
body = get_json("/rate/#{from}/#{to}", "date" => date.to_s)
raise Error, "Unexpected Frankfurter response shape" unless body.is_a?(Hash) && body["rate"]
begin
parsed_date = Date.parse(body["date"].to_s)
rescue Date::Error => e
raise Error, "Invalid date in Frankfurter response: #{e.message}"
end
Rate.new(date: parsed_date, from: from, to: to, rate: body["rate"].to_f)
end
end
end
def fetch_exchange_rates(from:, to:, start_date:, end_date:)
from = sanitize_currency(from)
to = sanitize_currency(to)
with_provider_response do
if from == toView on GitHub (pinned to e69894adb9)
Solutions
- Reproduce: curl "https://api.frankfurter.dev/v2/rate/USD/EUR?date=2025-01-02" and verify the JSON has date/base/quote/rate
- Confirm FRANKFURTER_URL ends with the correct version path (/v2) for the expected response shape
- Check that both currencies are in GET /currencies — ECB covers a fixed set; exotic pairs will never resolve
- If a mirror is in use, switch back to the official host to rule out divergent API versions
- Log the raw body on failure so unsupported-pair responses are distinguishable from format changes
Example fix
# before: opaque shape failure
raise Error, "Unexpected Frankfurter response shape" unless body.is_a?(Hash) && body["rate"]
# after: include the pair and body excerpt for diagnosis
unless body.is_a?(Hash) && body["rate"]
raise Error, "Unexpected Frankfurter response shape for #{from}/#{to}: #{body.to_s.truncate(200)}"
end Defensive patterns
Strategy: type-guard
Validate before calling
# Pre-check that the pair is supported before requesting a rate def frankfurter_supports?(cur) SUPPORTED = provider.send(:get_json, "/currencies").keys.map(&:upcase) SUPPORTED.include?(cur.upcase) end return Rate.new(date:, from:, to:, rate: 1.0) unless frankfurter_supports?(from) && frankfurter_supports?(to)
Type guard
# Narrow a Frankfurter single-rate body before use def valid_rate_body?(body) body.is_a?(Hash) && body["rate"].is_a?(Numeric) && body["date"].is_a?(String) end
Try / catch
begin
rate = provider.fetch_exchange_rate(from: "USD", to: "EUR", date: date)
rescue Provider::Frankfurter::Error => e
raise unless e.message.include?("Unexpected Frankfurter response shape")
notify_ops("Frankfurter rate shape drift for USD/EUR")
nil
end Prevention
- Validate currencies against GET /currencies before requesting rates for exotic pairs
- Keep FRANKFURTER_URL version-pinned (/v2) — v1 has a different /rate shape
- Distinguish 'unsupported pair' from 'API changed' by logging bodies on shape failures
- For USD/EUR-style majors, cache rates; only edge pairs risk shape surprises
When it happens
Trigger: Calling fetch_exchange_rate(from:, to:, date:) with two distinct sanitized currencies where the response body is an error envelope, an HTML page parsed into an odd shape, or a Frankfurter API version change (e.g. FRANKFURTER_URL pointing at v1 whose /rate response shape differs). Also fires if the pair is unsupported and the service returns JSON without a 'rate' field.
Common situations: Switching FRANKFURTER_URL between api.frankfurter.dev versions or self-hosted mirrors with different route shapes, requesting exotic currency pairs Frankfurter (ECB data) does not cover, gateways returning JSON status objects, and reverse proxies serving error pages with 200 status.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Unexpected Frankfurter response shape (expected an array)
- Unexpected response format from search API
- Unexpected response format from EOD API
- Invalid date in Frankfurter response: #{e.message}
- Invalid Frankfurter response: #{e.message}
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/4e9013801418241c.
Report an issue: GitHub.