{"record":{"id":"64dae1e7e4fd06cd","repo":"instructure/canvas-lms","slug":"invalid-bymonth-bymonth","errorCode":null,"errorMessage":"Invalid BYMONTH '%{bymonth}'","messagePattern":"Invalid BYMONTH '%(.+?)'","errorType":"validation","errorClass":"RruleValidationError","httpStatus":null,"severity":"error","filePath":"app/helpers/rrule_helper.rb","lineNumber":132,"sourceCode":"  def join_month_dys(days)\n    days.join(\",\")\n  end\n\n  def parse_byday(byday)\n    byday.split(\",\").map do |d|\n      match = /\\A([-+]?\\d+)?([A-Z]{2})\\z/.match(d)\n      raise RruleValidationError, I18n.t(\"Invalid BYDAY '%{byday}'\", byday:) unless match\n\n      {\n        occurrence: match[1].to_i,\n        day_of_week: DAYS_OF_WEEK[match[2]]\n      }\n    end\n  end\n\n  def parse_bymonth(bymonth)\n    month = bymonth.to_i\n    raise RruleValidationError, I18n.t(\"Invalid BYMONTH '%{bymonth}'\", bymonth:) unless month.between?(1, 12)\n\n    month\n  end\n\n  def parse_bymonthday(bymonthday, month)\n    raise RruleValidationError, I18n.t(\"Unsupported BYMONTHDAY, only a single day is permitted.\") unless bymonthday.split(\",\").length == 1\n\n    monthday = bymonthday.to_i\n\n    # not validating if we're in a leap year\n    raise RruleValidationError, I18n.t(\"Invalid BYMONTHDAY '%{bymonthday}'\", bymonthday:) unless monthday.between?(1, DAYS_IN_MONTH[month])\n\n    monthday\n  end\n\n  def format_date(date_str)\n    date = date_str.split(\"T\")[0]\n    year = date[0, 4].to_i","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/helpers/rrule_helper.rb#L114-L150","documentation":"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.","triggerScenarios":"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)).","commonSituations":"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.","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"],"exampleFix":"// before\n\"FREQ=YEARLY;INTERVAL=1;BYMONTH=0\" // JS getMonth() leaked through\n// after\n\"FREQ=YEARLY;INTERVAL=1;BYMONTH=12\"","handlingStrategy":"validation","validationCode":"month = bymonth.to_i\nraise ArgumentError, \"BYMONTH must be 1-12\" unless month.between?(1, 12)","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":["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"],"tags":["rrule","validation","bymonth","calendar"],"backgroundTag":"value-out-of-range","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"}