we-promise/sure · error · Provider::Frankfurter::Error

Invalid date in Frankfurter response: #{e.message}

Error message

Invalid date in Frankfurter response: #{e.message}

What it means

Raised as Provider::Frankfurter::Error when the 'date' field of a successful /rate/<from>/<to> response cannot be parsed by Date.parse (a Date::Error). Frankfurter carries forward weekend/holiday rates, so the returned date may differ from the requested one but is expected to always be a valid ISO date; if the service ever returns null, an empty string, or a non-date string, this guard converts the Ruby parse failure into a provider error naming the underlying message.

Source

Thrown at app/models/provider/frankfurter.rb:59

  # 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 == to
        generate_same_currency_rates(from, to, start_date, end_date)
      else
        exchange_rates(from, to, start_date, end_date)
      end
    end

View on GitHub (pinned to e69894adb9)

Solutions

  1. curl the exact URL with the failing date and inspect the 'date' field: curl "https://api.frankfurter.dev/v2/rate/USD/EUR?date=1990-01-01"
  2. If requesting very old dates, clamp start dates to the provider's history window (ECB data begins 1999-01-04)
  3. If a mirror returns null dates, switch FRANKFURTER_URL to the official api.frankfurter.dev host
  4. Validate the requested date is a real calendar date before calling (catch typos like 2025-13-01 earlier)
  5. If upstream changed the date format, parse the new format explicitly with Date.strptime(body['date'], '%Y-%m-%d') and a clear error

Example fix

# before: any Date.parse failure is fatal
parsed_date = Date.parse(body["date"].to_s)

# after: reject blank early with a targeted message, then parse strictly
raw = body["date"].to_s
raise Error, "Frankfurter returned no date for rate" if raw.blank?
parsed_date = Date.strptime(raw, "%Y-%m-%d")
Defensive patterns

Strategy: validation

Validate before calling

# Clamp requested dates to Frankfurter's history window and sanity-check format
MIN_FRANKFURTER_DATE = Date.new(1999, 1, 4)

def valid_rate_request_date?(date)
  date.is_a?(Date) && date >= MIN_FRANKFURTER_DATE && date <= Date.current
end

Type guard

# Strict parse of the expected ISO date field
def parse_frankfurter_date(value)
  return nil unless value.is_a?(String) && value.match?(\A\d{4}-\d{2}-\d{2}\z)
  Date.strptime(value, "%Y-%m-%d")
end

Try / catch

begin
  rate = provider.fetch_exchange_rate(from: from, to: to, date: date)
rescue Provider::Frankfurter::Error => e
  raise unless e.message.include?("Invalid date")
  Rails.logger.warn("Frankfurter bad date for #{from}/#{to} @ #{date}: #{e.message}")
  nil
end

Prevention

When it happens

Trigger: Calling fetch_exchange_rate where body['date'] is missing, empty, null, or in an unparseable format — e.g. an API change returning a timestamp instead of 'YYYY-MM-DD', a mirror returning {'date': null} for dates before its data start, or a response envelope where 'date' holds an object.

Common situations: Requesting rates before Frankfurter's earliest available date (ECB history starts 1999), mirrors with partial datasets, upstream format regressions, and Date.parse edge cases like ambiguous strings. Note Ruby's Date.parse raising Date::Error (not ArgumentError) requires Ruby >= 3.2 style handling — this rescue targets that.

Related errors


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