{"record":{"id":"9cfaf849ae02fcd0","repo":"we-promise/sure","slug":"invalid-date-in-frankfurter-response-e-message","errorCode":null,"errorMessage":"Invalid date in Frankfurter response: #{e.message}","messagePattern":"Invalid date in Frankfurter response: #(.+?)","errorType":"exception","errorClass":"Provider::Frankfurter::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/frankfurter.rb","lineNumber":59,"sourceCode":"\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\n      if from == to\n        Rate.new(date: date, from: from, to: to, rate: 1.0)\n      else\n        body = get_json(\"/rate/#{from}/#{to}\", \"date\" => date.to_s)\n        raise Error, \"Unexpected Frankfurter response shape\" unless body.is_a?(Hash) && body[\"rate\"]\n\n        begin\n          parsed_date = Date.parse(body[\"date\"].to_s)\n        rescue Date::Error => e\n          raise Error, \"Invalid date in Frankfurter response: #{e.message}\"\n        end\n\n        Rate.new(date: parsed_date, from: from, to: to, rate: body[\"rate\"].to_f)\n      end\n    end\n  end\n\n  def fetch_exchange_rates(from:, to:, start_date:, end_date:)\n    from = sanitize_currency(from)\n    to = sanitize_currency(to)\n\n    with_provider_response do\n      if from == to\n        generate_same_currency_rates(from, to, start_date, end_date)\n      else\n        exchange_rates(from, to, start_date, end_date)\n      end\n    end","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/frankfurter.rb#L41-L77","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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\"","If requesting very old dates, clamp start dates to the provider's history window (ECB data begins 1999-01-04)","If a mirror returns null dates, switch FRANKFURTER_URL to the official api.frankfurter.dev host","Validate the requested date is a real calendar date before calling (catch typos like 2025-13-01 earlier)","If upstream changed the date format, parse the new format explicitly with Date.strptime(body['date'], '%Y-%m-%d') and a clear error"],"exampleFix":"# before: any Date.parse failure is fatal\nparsed_date = Date.parse(body[\"date\"].to_s)\n\n# after: reject blank early with a targeted message, then parse strictly\nraw = body[\"date\"].to_s\nraise Error, \"Frankfurter returned no date for rate\" if raw.blank?\nparsed_date = Date.strptime(raw, \"%Y-%m-%d\")","handlingStrategy":"validation","validationCode":"# Clamp requested dates to Frankfurter's history window and sanity-check format\nMIN_FRANKFURTER_DATE = Date.new(1999, 1, 4)\n\ndef valid_rate_request_date?(date)\n  date.is_a?(Date) && date >= MIN_FRANKFURTER_DATE && date <= Date.current\nend","typeGuard":"# Strict parse of the expected ISO date field\ndef parse_frankfurter_date(value)\n  return nil unless value.is_a?(String) && value.match?(\\A\\d{4}-\\d{2}-\\d{2}\\z)\n  Date.strptime(value, \"%Y-%m-%d\")\nend","tryCatchPattern":"begin\n  rate = provider.fetch_exchange_rate(from: from, to: to, date: date)\nrescue Provider::Frankfurter::Error => e\n  raise unless e.message.include?(\"Invalid date\")\n  Rails.logger.warn(\"Frankfurter bad date for #{from}/#{to} @ #{date}: #{e.message}\")\n  nil\nend","preventionTips":["Clamp FX date requests to the provider's history window (ECB data from 1999-01-04)","Parse response dates strictly with Date.strptime('%Y-%m-%d') instead of Date.parse","Validate user-supplied dates before passing them through to provider calls","Watch for upstream format changes by logging unparseable values verbatim"],"tags":["frankfurter","date-parsing","fx-rates","api-contract"],"backgroundTag":"date-parsing-error","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}