phacility/phabricator · error · ConduitException
ERR-BAD-DIFF
ERR-BAD-DIFF
Error message
ERR-BAD-DIFF
What it means
Thrown by differential.createinline when both revisionID and diffID are supplied. The diff is loaded directly (id(new DifferentialDiff())->load($did), a legacy load with no policy filtering) and must both exist and satisfy diff->getRevisionID() == rid. Failing either check throws ConduitException('ERR-BAD-DIFF').
Source
Thrown at src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php:66
if ($rid) {
// Given both a revision and a diff, check that they match.
// Given only a revision, find the active diff.
$revision = id(new DifferentialRevisionQuery())
->setViewer($request->getUser())
->withIDs(array($rid))
->executeOne();
if (!$revision) {
throw new ConduitException('ERR-BAD-REVISION');
}
if (!$did) { // did not!
$diff = $revision->loadActiveDiff();
$did = $diff->getID();
} else { // did too!
$diff = id(new DifferentialDiff())->load($did);
if (!$diff || $diff->getRevisionID() != $rid) {
throw new ConduitException('ERR-BAD-DIFF');
}
}
} else if ($did) {
// Given only a diff, find the parent revision.
$diff = id(new DifferentialDiff())->load($did);
if (!$diff) {
throw new ConduitException('ERR-BAD-DIFF');
}
$rid = $diff->getRevisionID();
} else {
// Given neither, bail.
throw new ConduitException('ERR-NEED-DIFF');
}
$file = $request->getValue('filePath');
if (!$file) {
throw new ConduitException('ERR-NEED-FILE');
}View on GitHub (pinned to 5720a38cfe)
Solutions
- Omit diffID when you pass revisionID - the server then uses the revision's active diff, which cannot mismatch
- Otherwise re-fetch the current diff ID for the revision (differential.getdiff with revision_id) before calling
- Never cache diff IDs across long-running jobs; refresh them per run
- When catching ERR-BAD-DIFF, re-resolve the active diff and retry once
Example fix
// before $params = array( 'revisionID' => 789, 'diffID' => 4210, // stale: belongs to another revision 'filePath' => 'src/app.php', ); // after $params = array( 'revisionID' => 789, // server picks the active diff 'filePath' => 'src/app.php', );
Defensive patterns
Strategy: validation
Validate before calling
// Prefer sending only revisionID (server uses the active diff).
// If you must pin a diff, verify it belongs to the revision first:
$info = $client->callMethodSynchronous('differential.getdiff',
array('diff_id' => (int)$did));
if ((int)$info['revision_id'] !== (int)$rid) {
unset($params['diffID']); // let the server pick the active diff
} Try / catch
try {
$client->callMethodSynchronous('differential.createinline', $params);
} catch (ConduitClientException $ex) {
if ($ex->getErrorCode() === 'ERR-BAD-DIFF') {
unset($params['diffID']); // retry once with the active diff
$client->callMethodSynchronous('differential.createinline', $params);
} else {
throw $ex;
}
} Prevention
- Send revisionID alone unless a specific historical diff is required
- Never cache diff IDs across revision updates
- When both IDs are sent, treat a mismatch as stale local state and re-resolve
When it happens
Trigger: Passing a diffID that does not exist alongside a valid revisionID, or a diffID that belongs to a different revision (stale cached diff ID, off-by-one when parsing arc output, diff from another working copy).
Common situations: Review bots caching diff IDs from earlier runs while the author uploads a new diff; mixing IDs between two revisions edited in parallel; scripts that reuse a diff ID after the revision was updated.
Related errors
- Field "changes" must be non-empty.
- Field "raw_diff" must be non-empty.
- The raw diff you have submitted is too large to parse (it af
- ERR_BAD_DIFF
- ERR_BAD_DIFF
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/31edaacb27f3adf8.
Report an issue: GitHub.