{"record":{"id":"858ea36c896a22cf","repo":"instructure/canvas-lms","slug":"missing-count-or-until","errorCode":null,"errorMessage":"Missing COUNT or UNTIL","messagePattern":"Missing COUNT or UNTIL","errorType":"validation","errorClass":"RruleValidationError","httpStatus":null,"severity":"error","filePath":"app/helpers/rrule_helper.rb","lineNumber":61,"sourceCode":"    else\n      raise RruleValidationError, I18n.t(\"Invalid FREQ '%{freq}'\", freq: rropts[\"FREQ\"])\n    end\n  rescue => e\n    logger.error \"RRULE to natural language failure: #{e}\"\n    nil\n  end\n\n  def rrule_parse(rrule)\n    Hash[*rrule.sub(/^RRULE:/, \"\").split(/[;=]/)]\n  end\n\n  def rrule_validate_common_opts(rropts)\n    raise RruleValidationError, I18n.t(\"Missing INTERVAL\") unless rropts.key?(\"INTERVAL\")\n    raise RruleValidationError, I18n.t(\"INTERVAL must be > 0\") unless rropts[\"INTERVAL\"].to_i > 0\n\n    # We do not support never ending series because each event in the series\n    # must get created in the db to support the paginated calendar_events api\n    raise RruleValidationError, I18n.t(\"Missing COUNT or UNTIL\") unless rropts.key?(\"COUNT\") || rropts.key?(\"UNTIL\")\n\n    if rropts.key?(\"COUNT\")\n      raise RruleValidationError, I18n.t(\"COUNT must be > 0\") unless rropts[\"COUNT\"].to_i > 0\n      raise RruleValidationError, I18n.t(\"COUNT must be <= %{limit}\", limit: RruleHelper::RECURRING_EVENT_LIMIT) unless rropts[\"COUNT\"].to_i <= RruleHelper::RECURRING_EVENT_LIMIT\n    else\n      begin\n        format_date(rropts[\"UNTIL\"])\n      rescue\n        raise RruleValidationError, I18n.t(\"Invalid UNTIL '%{until_date}'\", until_date: rropts[\"UNTIL\"])\n      end\n    end\n  end\n\n  private\n\n  DAYS_OF_WEEK = {\n    \"SU\" => I18n.t(\"Sun\"),\n    \"MO\" => I18n.t(\"Mon\"),","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/helpers/rrule_helper.rb#L43-L79","documentation":"RruleHelper validates recurring-event RRULE options before converting them to natural language. Canvas does not support never-ending event series because every event in the series must be materialized in the database to support the paginated calendar_events API, so an RRULE must terminate via COUNT or UNTIL. If the rropts hash contains neither key, rrule_validate_common_opts raises RruleValidationError with this message.","triggerScenarios":"Calling RruleHelper.rrule_to_natural_language with an rropts hash that has FREQ (and valid INTERVAL) but omits both the \"COUNT\" and \"UNTIL\" keys — e.g. an RRULE string like FREQ=DAILY;INTERVAL=1 with no termination clause.","commonSituations":"Importing or parsing external calendar data (ICS files) whose RRULEs omit COUNT/UNTIL because they were designed as infinite series; clients constructing recurring calendar events via the API and forgetting the end condition; upstream calendar libraries that allow open-ended recurrence.","solutions":["Add a COUNT (number of occurrences) or UNTIL (end date) clause to the RRULE so the series terminates","If the series is meant to be infinite, split it into bounded segments (e.g. COUNT=365 chunks) since Canvas caps series via RruleHelper::RECURRING_EVENT_LIMIT","Validate the RRULE before saving the event and return a 400 to the client explaining that recurring events must have an end"],"exampleFix":"// before\n\"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO\"\n// after\n\"FREQ=WEEKLY;INTERVAL=1;BYDAY=MO;COUNT=10\"","handlingStrategy":"validation","validationCode":"def valid_rrule_termination?(rropts)\n  rropts.key?(\"COUNT\") || rropts.key?(\"UNTIL\")\nend\nraise ArgumentError, \"RRULE needs COUNT or UNTIL\" unless valid_rrule_termination?(rropts)","typeGuard":null,"tryCatchPattern":"begin\n  RruleHelper.rrule_to_natural_language(rropts)\nrescue RruleValidationError => e\n  render json: { errors: [e.message] }, status: :bad_request\nend","preventionTips":["Always emit COUNT or UNTIL when building RRULEs","Treat infinite recurrence as unsupported in Canvas","Validate RRULE strings at the API boundary before persistence"],"tags":["rrule","validation","calendar","recurrence"],"backgroundTag":"missing-required-argument","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}