phacility/phabricator · error · Exception
Expected ISO8601 duration in the format "P12DT3H4M5S", found
Error message
Expected ISO8601 duration in the format "P12DT3H4M5S", found "%s".
What it means
PhutilCalendarDuration's ISO 8601 duration parser accepts an optional sign, a literal 'P', then either a weeks component (nW) or days and/or time components (nD, T nH nM nS). Values missing the P, missing every component, or using unsupported units (Y years, M months) fail the regex and throw.
Source
Thrown at src/applications/calendar/parser/data/PhutilCalendarDuration.php:75
public static function newFromISO8601($value) {
$pattern =
'/^'.
'(?P<sign>[+-])?'.
'P'.
'(?:'.
'(?P<W>\d+)W'.
'|'.
'(?:(?:(?P<D>\d+)D)?'.
'(?:T(?:(?P<H>\d+)H)?(?:(?P<M>\d+)M)?(?:(?P<S>\d+)S)?)?'.
')'.
')'.
'\z/';
$matches = null;
$ok = preg_match($pattern, $value, $matches);
if (!$ok) {
throw new Exception(
pht(
'Expected ISO8601 duration in the format "P12DT3H4M5S", found '.
'"%s".',
$value));
}
$is_negative = (idx($matches, 'sign') == '-');
return id(new self())
->setIsNegative($is_negative)
->setWeeks((int)idx($matches, 'W', 0))
->setDays((int)idx($matches, 'D', 0))
->setHours((int)idx($matches, 'H', 0))
->setMinutes((int)idx($matches, 'M', 0))
->setSeconds((int)idx($matches, 'S', 0));
}
public function toISO8601() {View on GitHub (pinned to 5720a38cfe)
Solutions
- Ensure the value has the leading P (with optional +/- sign) and at least one component: 'PT30M', 'P1D', 'P2W', '-P1D'.
- Convert unsupported units before parsing: years/months to days/weeks (e.g. P1Y -> P52W or P365D per your business rule).
- Insert the required T before hour/minute/second parts when a date part is also present: 'P1DT2H'.
- Validate with the production regex before feeding external data in.
Example fix
// before
$dur = PhutilCalendarDuration::newFromISO8601('30M');
// after
$dur = PhutilCalendarDuration::newFromISO8601('PT30M'); Defensive patterns
Strategy: type-guard
Validate before calling
$value = strtoupper(trim($value));
if (!preg_match('/^[+-]?P(\d+W|(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?)$/', $value)
|| preg_match('/^[+-]?P[^WDT]/', $value)) {
// rewrite unsupported units (Y/M) to days/weeks per your rules first
$value = preg_replace('/(\d+)Y/', '${1}365D', $value);
$value = preg_replace('/(\d+)M(?!\d*S)/', '${1}30D', $value);
} Type guard
function isIso8601Duration($value) {
return (bool) preg_match(
'/^[+-]?P(\d+W|\d+D(T(\d+H)?(\d+M)?(\d+S)?)?|T(\d+H)?(\d+M)?(\d+S)?)$/',
strtoupper(trim((string)$value))
);
} Try / catch
try {
$dur = PhutilCalendarDuration::newFromISO8601($value);
} catch (Exception $ex) {
phlog('Bad ISO8601 duration: '.$value);
continue; // skip this property, keep importing
} Prevention
- Always emit the leading P and the T before time components.
- Reject or convert Y/M units before feeding external durations in.
- Unit-test your duration writer against the parser's accepted forms (P2W, P1DT2H, PT30M, -P1D).
When it happens
Trigger: newFromISO8601('3H') (missing P), 'PT' or 'P' (no components), 'P1Y6M' (years/months unsupported), 'P1.5H' (fraction), 'P1D2H' (T missing before time parts).
Common situations: ICS DURATION values from producers that emit years/months; human-written shorthand like '30m'; forgetting the T separator between date and time parts.
Related errors
- Expected ISO8601 datetime in the format "19990105T112233Z",
- ISO8601 date ends in "Z" indicating UTC, but a timezone othe
- RRULE dictionary includes unknown key "%s". Expected keys ar
- Unexpected value "%s" in "%s" RULE property: expected an int
- Unexpected value "%s" in "%s" RRULE property: expected only
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/2d310d32a6529fee.
Report an issue: GitHub.