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

  1. Fetch the failing URL with curl -i and check Content-Type — HTML means proxy/misroute, not an API issue
  2. Verify FRANKFURTER_URL includes the versioned base (default https://api.frankfurter.dev/v2)
  3. If the body is an upstream 5xx page, rely on the existing Faraday retry (3 attempts, backoff) and retry the job later
  4. If a corporate proxy intercepts HTTPS, configure Faraday's proxy or exclude the host from SSL inspection
  5. 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

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


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