instructure/canvas-lms · error · RruleValidationError

Invalid UNTIL

Error message

Invalid UNTIL '%{until_date}'

What it means

When no COUNT key is present, the UNTIL branch attempts format_date(rropts["UNTIL"]). If that parsing raises (missing/blank value, non-date string, unexpected format), the rescue block re-raises RruleValidationError with "Invalid UNTIL '%{until_date}'" including the raw value.

Solutions

  1. Send UNTIL as an ISO-8601 date or datetime string, e.g. UNTIL=20241231T235959Z or 2024-12-31
  2. Verify the RRULE parsing code is not stripping or corrupting the UNTIL value before validation
  3. Prefer COUNT when the end condition is occurrence-count based and formatting dates is error-prone

Example fix

// before
"FREQ=DAILY;INTERVAL=1;UNTIL=tomorrow"
// after
"FREQ=DAILY;INTERVAL=1;UNTIL=2024-12-31T23:59:59Z"
Defensive patterns

Strategy: validation

Validate before calling

until_val = rropts["UNTIL"]
raise ArgumentError, "invalid UNTIL" unless until_val.is_a?(String) && until_val.match?(/\A\d{4}-?\d{2}-?\d{2}(T.*)?\z/)

Try / catch

begin
  RruleHelper.rrule_to_natural_language(rropts)
rescue RruleValidationError => e
  render json: { errors: [e.message] }, status: :bad_request
end

Prevention

When it happens

Trigger: Calling rrule_to_natural_language with "UNTIL" => "" , "UNTIL" => "not-a-date", or a value whose split("T")[0] portion cannot be parsed by format_date — while "COUNT" is absent from rropts.

Common situations: Clients send UNTIL in a non-ISO format (e.g. '2024-12-31 23:59:59' with a space, or a human-readable date); blank UNTIL from an unset form field; ICS exports with UNTIL values in formats format_date does not accept.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/69612fcf474422ec. Report an issue: GitHub.

Appendix: source

Thrown at app/helpers/rrule_helper.rb:70

    Hash[*rrule.sub(/^RRULE:/, "").split(/[;=]/)]
  end

  def rrule_validate_common_opts(rropts)
    raise RruleValidationError, I18n.t("Missing INTERVAL") unless rropts.key?("INTERVAL")
    raise RruleValidationError, I18n.t("INTERVAL must be > 0") unless rropts["INTERVAL"].to_i > 0

    # We do not support never ending series because each event in the series
    # must get created in the db to support the paginated calendar_events api
    raise RruleValidationError, I18n.t("Missing COUNT or UNTIL") unless rropts.key?("COUNT") || rropts.key?("UNTIL")

    if rropts.key?("COUNT")
      raise RruleValidationError, I18n.t("COUNT must be > 0") unless rropts["COUNT"].to_i > 0
      raise RruleValidationError, I18n.t("COUNT must be <= %{limit}", limit: RruleHelper::RECURRING_EVENT_LIMIT) unless rropts["COUNT"].to_i <= RruleHelper::RECURRING_EVENT_LIMIT
    else
      begin
        format_date(rropts["UNTIL"])
      rescue
        raise RruleValidationError, I18n.t("Invalid UNTIL '%{until_date}'", until_date: rropts["UNTIL"])
      end
    end
  end

  private

  DAYS_OF_WEEK = {
    "SU" => I18n.t("Sun"),
    "MO" => I18n.t("Mon"),
    "TU" => I18n.t("Tue"),
    "WE" => I18n.t("Wed"),
    "TH" => I18n.t("Thu"),
    "FR" => I18n.t("Fri"),
    "SA" => I18n.t("Sat")
  }.freeze
  MONTHS = [
    nil,
    I18n.t("January"),

View on GitHub (pinned to 1c9f0bb801)