phacility/phabricator · error · Exception

Transaction specifies both "beforePHID" and "beforePHIDs". S

Error message

Transaction specifies both "beforePHID" and "beforePHIDs". Specify only "beforePHIDs".

What it means

The mirror of the 'after' rule: a column move transaction may use the modern 'beforePHIDs' list or the deprecated 'beforePHID' single string, never both. During normalization in buildMoveTransaction, a non-empty 'beforePHIDs' alongside an isset 'beforePHID' throws this Exception.

Source

Thrown at src/applications/maniphest/editor/ManiphestTransactionEditor.php:490

        'afterPHIDs' => array(),
      );

      // Normalize the legacy keys "beforePHID" and "afterPHID" keys to the
      // modern format.
      if (!empty($value['afterPHID'])) {
        if ($value['afterPHIDs']) {
          throw new Exception(
            pht(
              'Transaction specifies both "afterPHID" and "afterPHIDs". '.
              'Specify only "afterPHIDs".'));
        }
        $value['afterPHIDs'] = array($value['afterPHID']);
        unset($value['afterPHID']);
      }

      if (isset($value['beforePHID'])) {
        if ($value['beforePHIDs']) {
          throw new Exception(
            pht(
              'Transaction specifies both "beforePHID" and "beforePHIDs". '.
              'Specify only "beforePHIDs".'));
        }
        $value['beforePHIDs'] = array($value['beforePHID']);
        unset($value['beforePHID']);
      }

      foreach ($value['beforePHIDs'] as $phid) {
        $relative_phids[] = $phid;
      }

      foreach ($value['afterPHIDs'] as $phid) {
        $relative_phids[] = $phid;
      }

      $new[$key] = $value;
    }

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Use only "beforePHIDs": ["PHID-TASK-aaa"] and remove 'beforePHID' from the payload.
  2. Make legacy-key normalization unconditional in your client: if you populate 'beforePHID', never also send 'beforePHIDs'.
  3. Remember the parallel 'afterPHID'/'afterPHIDs' rule — fixing one side alone still leaves the transaction able to throw.

Example fix

// before
$value = array(
  'columnPHID'  => $column_phid,
  'beforePHID'  => $before_phid,
  'beforePHIDs' => array($before_phid),
);

// after
$value = array(
  'columnPHID'  => $column_phid,
  'beforePHIDs' => array($before_phid),
);
Defensive patterns

Strategy: validation

Validate before calling

if (isset($move['beforePHID'])) {
  if (!empty($move['beforePHIDs'])) {
    unset($move['beforePHID']); // conflicting formats; keep the list
  } else {
    $move['beforePHIDs'] = array($move['beforePHID']);
    unset($move['beforePHID']);
  }
}

Type guard

function has_no_legacy_position_keys(array $move) {
  return !isset($move['beforePHID']) && !isset($move['afterPHID']);
}

Prevention

When it happens

Trigger: A move value like {"columnPHID": "PHID-PCOL-...", "beforePHID": "PHID-TASK-aaa", "beforePHIDs": ["PHID-TASK-bbb"]} — typically from code that sets the legacy key first and then unconditionally initializes the list key.

Common situations: Refactoring old single-relative-position code to multi-position and leaving the old assignment on a code path that also populates the list. Default-value builders ($value += array('beforePHIDs' => ...)) combined with caller-supplied 'beforePHID'. Same-era documentation mixing both key styles.

Related errors


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