instructure/canvas-lms · error · RruleValidationError
Invalid BYMONTH
Error message
Invalid BYMONTH '%{bymonth}' What it means
parse_bymonth converts the BYMONTH value with to_i and requires it to be between 1 and 12 (January–December). Values outside that range — including non-numeric strings that become 0 — raise RruleValidationError with the raw value interpolated.
Solutions
- Send BYMONTH as an integer 1–12 (1 = January, 12 = December)
- Convert zero-based JS month values before building the RRULE: month + 1
- Validate month inputs at the API boundary before constructing the RRULE string
Example fix
// before "FREQ=YEARLY;INTERVAL=1;BYMONTH=0" // JS getMonth() leaked through // after "FREQ=YEARLY;INTERVAL=1;BYMONTH=12"
Defensive patterns
Strategy: validation
Validate before calling
month = bymonth.to_i raise ArgumentError, "BYMONTH must be 1-12" unless month.between?(1, 12)
Try / catch
begin
RruleHelper.rrule_to_natural_language(rropts)
rescue RruleValidationError => e
render json: { errors: [e.message] }, status: :bad_request
end Prevention
- Remember BYMONTH is 1-based; add 1 to JS getMonth() output
- Reject non-numeric month inputs at the form level
- Never emit BYMONTH=0 as a default
When it happens
Trigger: Calling rrule_to_natural_language with a yearly RRULE where BYMONTH is 0, 13, "abc", or an empty string (to_i => 0, failing between?(1,12)).
Common situations: Client confusion between zero-based month indexes (0–11 from JS Date.getMonth()) and RFC-5545 one-based months; parsing errors emitting empty BYMONTH; UIs storing month offsets instead of month numbers.
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/64dae1e7e4fd06cd.
Report an issue: GitHub.
Appendix: source
Thrown at app/helpers/rrule_helper.rb:132
def join_month_dys(days)
days.join(",")
end
def parse_byday(byday)
byday.split(",").map do |d|
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_iView on GitHub (pinned to 1c9f0bb801)