{"record":{"id":"3c94364fd38999f7","repo":"phacility/phabricator","slug":"rrule-specifies-bysetpos-with-freq-s-but-this","errorCode":null,"errorMessage":"RRULE specifies BYSETPOS with FREQ \"%s\", but this is invalid.","messagePattern":"RRULE specifies BYSETPOS with FREQ \"(.+?)\", but this is invalid\\.","errorType":"validation","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/calendar/parser/data/PhutilCalendarRecurrenceRule.php","lineNumber":671,"sourceCode":"        case self::FREQUENCY_WEEKLY:\n          $this->cursorWeek -= $interval;\n          $this->rewindWeek();\n          break;\n        case self::FREQUENCY_DAILY:\n          $this->cursorDay -= $interval;\n          $this->rewindDay();\n          break;\n        case self::FREQUENCY_HOURLY:\n          $this->cursorHour -= $interval;\n          $this->rewindHour();\n          break;\n        case self::FREQUENCY_MINUTELY:\n          $this->cursorMinute -= $interval;\n          $this->rewindMinute();\n          break;\n        case self::FREQUENCY_SECONDLY:\n        default:\n          throw new Exception(\n            pht(\n              'RRULE specifies BYSETPOS with FREQ \"%s\", but this is invalid.',\n              $frequency));\n      }\n    }\n\n    // We can generate events from before the cursor when evaluating rules\n    // with BYSETPOS or FREQ=WEEKLY.\n    $this->minimumEpoch = $this->getStartDateTime()->getEpoch();\n\n    $cursor_state = array(\n      'year' => $this->cursorYear,\n      'month' => $this->cursorMonth,\n      'week' => $this->cursorWeek,\n      'day' => $this->cursorDay,\n      'hour' => $this->cursorHour,\n    );\n","sourceCodeStart":653,"sourceCodeEnd":689,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/calendar/parser/data/PhutilCalendarRecurrenceRule.php#L653-L689","documentation":"Evaluating BYSETPOS requires the rule engine to rewind its cursor one interval, and PhutilCalendarRecurrenceRule only implements rewinding down to MINUTELY. The default branch of the rewind switch therefore throws for FREQ=SECONDLY (or any unrecognized frequency) rather than silently mis-evaluating the set positions.","triggerScenarios":"An RRULE that combines BYSETPOS with a sub-minute frequency, e.g. 'FREQ=SECONDLY;BYSETPOS=1', or code that sets frequency to FREQUENCY_SECONDLY together with setBySetPosition(...) and then triggers evaluation/rewind.","commonSituations":"Rules synthesized by test harnesses or tooling that sweep all frequencies; malformed imported RRULEs where FREQ was truncated (e.g. 'FREQ=SECOND;...' parsed into an unexpected value).","solutions":["Use a coarser frequency (MINUTELY or larger) when BYSETPOS is required","Drop the BYSETPOS part if second-level recurrence is the real requirement","Reject FREQ=SECONDLY + BYSETPOS combinations during RRULE pre-validation before import"],"exampleFix":"// before\n$rrule = 'FREQ=SECONDLY;BYSECOND=0,30;BYSETPOS=1';\n\n// after\n$rrule = 'FREQ=MINUTELY;BYSECOND=0,30;BYSETPOS=1';","handlingStrategy":"validation","validationCode":"if (isset($parts['BYSETPOS']) && idx($parts, 'FREQ') === 'SECONDLY') {\n  throw new Exception('BYSETPOS cannot be combined with FREQ=SECONDLY.');\n}","typeGuard":null,"tryCatchPattern":"try {\n  $events = $rule->getEvents();\n} catch (Exception $ex) {\n  if (strpos($ex->getMessage(), 'BYSETPOS') !== false) {\n    $events = $rule->setBySetPosition(array())->getEvents();\n  }\n}","preventionTips":["Bound supported frequencies in your product surface (most UIs never need SECONDLY)","Reject sub-minute frequencies at the input layer unless explicitly supported","Fuzz-test generated RRULEs across all FREQ values during development"],"tags":["rrule","bysetpos","icalendar","calendar"],"backgroundTag":"rrule-validation-failed","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}