instructure/canvas-lms · error · RruleValidationError

Unsupported BYMONTHDAY, only a single day is permitted.

Error message

Unsupported BYMONTHDAY, only a single day is permitted.

What it means

Raised by the generic validation guard in parse_bymonthday when parsing a yearly RRULE's BYMONTHDAY part: Canvas only supports a single month day (e.g. '15'), so the input at fault is a comma-separated BYMONTHDAY list (e.g. '1,15') supplied by the calendar/RRULE payload.

Solutions

  1. Use a single BYMONTHDAY value, e.g. BYMONTHDAY=15
  2. Model multiple days as multiple separate recurring calendar events (one per day)
  3. Reject or split multi-day rules at the API boundary with a clear client-facing message

Example fix

// before
"FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=1,15"
// after
"FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=15"
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, "only a single BYMONTHDAY is supported" unless bymonthday.split(",").length == 1

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 a yearly RRULE whose BYMONTHDAY contains a comma-separated list, e.g. BYMONTHDAY=1,15 — any value where split(",").length != 1.

Common situations: Clients copying RFC-5545 examples that legally allow lists (BYMONTHDAY=1,15) and expecting Canvas to support them; UIs allowing multi-select of days; importing ICS files with multi-day yearly rules.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at app/helpers/rrule_helper.rb:138

      match = /\A([-+]?\d+)?([A-Z]{2})\z/.match(d)
      raise RruleValidationError, I18n.t("Invalid BYDAY '%{byday}'", byday:) unless match

      {
        occurrence: match[1].to_i,
        day_of_week: DAYS_OF_WEEK[match[2]]
      }
    end
  end

  def parse_bymonth(bymonth)
    month = bymonth.to_i
    raise RruleValidationError, I18n.t("Invalid BYMONTH '%{bymonth}'", bymonth:) unless month.between?(1, 12)

    month
  end

  def parse_bymonthday(bymonthday, month)
    raise RruleValidationError, I18n.t("Unsupported BYMONTHDAY, only a single day is permitted.") unless bymonthday.split(",").length == 1

    monthday = bymonthday.to_i

    # not validating if we're in a leap year
    raise RruleValidationError, I18n.t("Invalid BYMONTHDAY '%{bymonthday}'", bymonthday:) unless monthday.between?(1, DAYS_IN_MONTH[month])

    monthday
  end

  def format_date(date_str)
    date = date_str.split("T")[0]
    year = date[0, 4].to_i
    month = date[4, 2].to_i
    day = date[6, 2].to_i

    I18n.l(Date.new(year, month, day), format: :medium)
  end

View on GitHub (pinned to 1c9f0bb801)