instructure/canvas-lms · error · RruleValidationError

Invalid BYMONTHDAY

Error message

Invalid BYMONTHDAY '%{bymonthday}'

What it means

After the single-day check, parse_bymonthday converts BYMONTHDAY to an integer and requires it to fall between 1 and the number of days in the given BYMONTH (DAYS_IN_MONTH lookup, leap years not considered here). Values like 0, negative numbers, non-numeric strings (to_i => 0), or 30 in February raise RruleValidationError.

Solutions

  1. Use BYMONTHDAY between 1 and the number of days in the chosen BYMONTH
  2. For February, cap at 29 (Canvas does not special-case leap years here)
  3. Model 'last day of month' semantics differently (e.g. BYDAY=-1SU-style or fixed day 28) since negative monthdays are invalid in this validation

Example fix

// before
"FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=30"
// after
"FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=28"
Defensive patterns

Strategy: validation

Validate before calling

days = { 1=>31, 2=>29, 3=>31, 4=>30, 5=>31, 6=>30, 7=>31, 8=>31, 9=>30, 10=>31, 11=>30, 12=>31 }
raise ArgumentError, "BYMONTHDAY out of range" unless bymonthday.to_i.between?(1, days[month])

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 where BYMONTHDAY is 0, -1, "x", 31 with BYMONTH=4 (April has 30), or 30 with BYMONTH=2.

Common situations: Clients modeling 'last day of month' with negative values (-1), which RFC-5545 allows but Canvas rejects; picking day 29/30/31 for February; typos like day 0 from off-by-one loops.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


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

Appendix: source

Thrown at app/helpers/rrule_helper.rb:143

        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

  def format_month_day(month, day)
    # 2024 is a leap year, and can handle formatting 2/29
    I18n.l(Date.new(2024, month, day), format: :short)
  end

View on GitHub (pinned to 1c9f0bb801)