phacility/phabricator · error · Exception

ISO8601 date ends in "Z" indicating UTC, but a timezone othe

Error message

ISO8601 date ends in "Z" indicating UTC, but a timezone other than UTC ("%s") was specified.

What it means

PhutilCalendarAbsoluteDateTime::newFromISO8601() throws when the parsed value ends in 'Z' (which asserts UTC) but a $timezone argument other than 'UTC' was also supplied. The two are contradictory, so the constructor refuses rather than silently choosing one.

Source

Thrown at src/applications/calendar/parser/data/PhutilCalendarAbsoluteDateTime.php:35

      '(?P<y>\d{4})(?P<m>\d{2})(?P<d>\d{2})'.
      '(?:'.
        'T(?P<h>\d{2})(?P<i>\d{2})(?P<s>\d{2})(?<z>Z)?'.
      ')?'.
      '\z/';

    $matches = null;
    $ok = preg_match($pattern, $value, $matches);
    if (!$ok) {
      throw new Exception(
        pht(
          'Expected ISO8601 datetime in the format "19990105T112233Z", '.
          'found "%s".',
          $value));
    }

    if (isset($matches['z'])) {
      if ($timezone != 'UTC') {
        throw new Exception(
          pht(
            'ISO8601 date ends in "Z" indicating UTC, but a timezone other '.
            'than UTC ("%s") was specified.',
            $timezone));
      }
    }

    $datetime = id(new self())
      ->setYear((int)$matches['y'])
      ->setMonth((int)$matches['m'])
      ->setDay((int)$matches['d'])
      ->setTimezone($timezone);

    if (isset($matches['h'])) {
      $datetime
        ->setHour((int)$matches['h'])
        ->setMinute((int)$matches['i'])
        ->setSecond((int)$matches['s']);

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Pass 'UTC' (or omit the parameter, UTC is the default) whenever the value ends in Z.
  2. If you need local time, keep timezone 'UTC' here and convert afterwards with setTimezone()/toLocalDateTime-style APIs.
  3. Strip the trailing Z and pre-convert the instant into the target timezone if you must pass a non-UTC zone.

Example fix

// before
$dt = PhutilCalendarAbsoluteDateTime::newFromISO8601('19990105T112233Z', 'America/New_York');
// after
$dt = PhutilCalendarAbsoluteDateTime::newFromISO8601('19990105T112233Z', 'UTC');
Defensive patterns

Strategy: validation

Validate before calling

// Reconcile the timezone with the value before parsing:
if (strtoupper(substr($value, -1)) === 'Z') {
  $timezone = 'UTC';
}
$dt = PhutilCalendarAbsoluteDateTime::newFromISO8601($value, $timezone);

Type guard

function timezoneMatchesValue($value, $timezone) {
  if (strtoupper(substr(trim($value), -1)) === 'Z') {
    return strcasecmp($timezone, 'UTC') === 0;
  }
  return true;
}

Try / catch

try {
  $dt = PhutilCalendarAbsoluteDateTime::newFromISO8601($value, $timezone);
} catch (Exception $ex) {
  // Z + non-UTC conflict: retry in UTC, then convert
  $dt = PhutilCalendarAbsoluteDateTime::newFromISO8601($value, 'UTC');
}

Prevention

When it happens

Trigger: newFromISO8601('19990105T112233Z', 'America/New_York'). Common when a helper always passes a default timezone while values arrive with Z suffixes.

Common situations: Reusable wrappers hardcoding a local timezone; ICS data mixing UTC (Z) values with a per-calendar timezone default; refactors that added a timezone parameter to existing call sites.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/6d481329c660d0f2. Report an issue: GitHub.