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
- Send UNTIL as an ISO-8601 date or datetime string, e.g. UNTIL=20241231T235959Z or 2024-12-31
- Verify the RRULE parsing code is not stripping or corrupting the UNTIL value before validation
- 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
- Always send UNTIL in ISO-8601 form
- Never send blank UNTIL strings — omit the key and use COUNT instead
- Normalize formats from ICS imports before validation
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)