we-promise/sure · error · Provider::Frankfurter::Error
Invalid Frankfurter response: #{e.message}
Error message
Invalid Frankfurter response: #{e.message} What it means
Raised as Provider::Frankfurter::Error when the response body for any Frankfurter request cannot be parsed as JSON (JSON::ParserError). get_json is the shared transport for /currencies, /rate and /rates; if the server returns HTML (error page), plain text, or an empty body, JSON.parse fails and the parser error is re-raised as a provider Error with the original message. A 5-second open timeout and 20-second read timeout plus a 3-retry Faraday layer sit below this guard.
Source
Thrown at app/models/provider/frankfurter.rb:104
# from/to are interpolated directly into the URL path in
# fetch_exchange_rate (GET /rate/{from}/{to}), so strip anything that
# isn't a letter before use - real ISO 4217 codes are always A-Z anyway.
def sanitize_currency(code)
code.to_s.upcase.gsub(/[^A-Z]/, "")
end
def base_url
ENV["FRANKFURTER_URL"].presence || "https://api.frankfurter.dev/v2"
end
def get_json(path, params = {})
throttle_request
response = client.get("#{base_url}#{path}") do |req|
params.each { |k, v| req.params[k] = v }
end
JSON.parse(response.body)
rescue JSON::ParserError => e
raise Error, "Invalid Frankfurter response: #{e.message}"
end
def client
@client ||= Faraday.new(url: base_url, ssl: self.class.faraday_ssl_options) do |faraday|
faraday.options.open_timeout = 5
faraday.options.timeout = 20
faraday.request(:retry, {
max: 3,
interval: 0.5,
interval_randomness: 0.5,
backoff_factor: 2,
exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [ Faraday::ConnectionFailed ]
})
faraday.request :json
faraday.response :raise_error
faraday.headers["Accept"] = "application/json"View on GitHub (pinned to e69894adb9)
Solutions
- Fetch the failing URL with curl -i and check Content-Type — HTML means proxy/misroute, not an API issue
- Verify FRANKFURTER_URL includes the versioned base (default https://api.frankfurter.dev/v2)
- If the body is an upstream 5xx page, rely on the existing Faraday retry (3 attempts, backoff) and retry the job later
- If a corporate proxy intercepts HTTPS, configure Faraday's proxy or exclude the host from SSL inspection
- Add the response status and a body excerpt to the raised error for faster triage
Example fix
# before: parser error message only, no context on what came back
rescue JSON::ParserError => e
raise Error, "Invalid Frankfurter response: #{e.message}"
# after: capture status + body excerpt before parsing
body_raw = response.body
parsed = begin
JSON.parse(body_raw)
rescue JSON::ParserError => e
raise Error, "Invalid Frankfurter response (HTTP #{response.status}): #{e.message} body=#{body_raw.to_s.truncate(120)}"
end Defensive patterns
Strategy: retry
Validate before calling
# Cheap pre-flight that the endpoint speaks JSON before a sync run
uri = URI("#{base}/currencies")
res = Net::HTTP.get_response(uri)
res.content_type == "application/json" Try / catch
begin
rates = provider.fetch_exchange_rates(...)
rescue Provider::Frankfurter::Error => e
raise unless e.message.include?("Invalid Frankfurter response")
# HTML/empty bodies are usually transient gateway errors — back off and retry once
sleep 5
retry
end Prevention
- Verify FRANKFURTER_URL points at the JSON API, not the marketing site
- Keep Faraday retry middleware enabled (3 attempts with backoff) for 5xx/parse failures
- Check Content-Type on responses in lower layers to fail fast on HTML error pages
- Exclude the API host from SSL-inspecting proxies that mangle bodies
When it happens
Trigger: Any Frankfurter call where the body is not valid JSON: a 502/503 HTML page from a proxy in front of the API, an empty body from a dropping connection, a rate-limit page from a mirror, or FRANKFURTER_URL pointing at a non-API host that serves a website at the same path.
Common situations: Transient gateway errors at frankfurter.dev (usually retried away by the Faraday middleware), misconfigured FRANKFURTER_URL (e.g. https://frankfurter.dev without /v2, returning HTML), captive portals or SSL-inspecting proxies injecting HTML, and self-hosted instances returning error pages while booting.
Related errors
- Unexpected Frankfurter response shape
- Could not save that passkey or security key. Please try agai
- Frankfurter currencies endpoint returned no data
- Invalid date in Frankfurter response: #{e.message}
- Unexpected Frankfurter response shape (expected an array)
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/d61b6fd87f376aee.
Report an issue: GitHub.