passbolt/passbolt_api · error · InternalErrorException
Tag ID " " is already V5
Error message
Tag ID "{0}" is already V5 What it means
MigrateAllV4TagsToV5Service::migrate converts V4 cleartext tags to V5 encrypted metadata. If the tag DTO already reports isV5(), the migration cannot proceed and an InternalErrorException naming the tag ID is thrown. This is an invariant check that prevents double-migration within a run.
Solutions
- Skip or filter tags already in V5 format before migrating (query tags where metadata IS NULL / v4 only).
- Make migrate idempotent: catch this condition per-tag, log it, and continue with remaining tags.
- Run the migration single-threaded to avoid concurrent double-processing.
- Verify tag state in the DB (metadata_key_id set) before scheduling a rerun.
Example fix
// before
if ($dto->isV5()) { throw new InternalErrorException(...); }
// after
if ($dto->isV5()) { $this->skipped++; return; } // idempotent skip Defensive patterns
Strategy: try-catch
Validate before calling
$v4Tags = $tagsTable->find()
->where(function ($exp) { return $exp->isNull('metadata'); })
->all(); Type guard
function needsMigration(array $tag): bool {
return empty($tag['metadata']) || empty($tag['metadata_key_id']);
} Try / catch
try {
$service->migrate($uac, $batch);
} catch (InternalErrorException $e) {
if (str_contains($e->getMessage(), 'is already V5')) {
$this->log('Skipping already-migrated tag; rerun with V4-only filter');
} else { throw $e; }
} Prevention
- Filter V4-only tags in the migration query
- Run migration jobs once, serialized
- Make per-tag migration idempotent (skip instead of throw)
- Track migration progress persistently between restarts
When it happens
Trigger: Re-running the v4-to-v5 tag migration while some tags were already converted in a previous (possibly partially completed) run; a tag persisted with metadata/metadata_key_id populated but processed again in the same batch.
Common situations: Migration job interrupted and restarted without filtering already-migrated tags; concurrent migration runs; stale batches cached between executions.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- No OpenPGP key found for the user. The metadata could not…
- No user found for the personal tag id
- Resource creation with cleartext metadata not allowed.
- Tag creation with cleartext metadata not allowed.
- The tag metadata key data could not be updated.
AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17).
Data as JSON: /api/errors/09abbdec7e5b9e0b.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/PassboltEe/Tags/src/Service/Metadata/MigrateAllV4TagsToV5Service.php:101
->contain(['Users.Gpgkeys'])
->where(['slug IS NOT NULL'])
->all()
->toArray();
if (count($tags) < 1) {
$this->addError(['error_message' => __('No tags to migrate.')]);
return $this->getResult();
}
/** @var \Passbolt\Tags\Model\Entity\Tag $tag */
foreach ($tags as $tag) {
$dto = MetadataTagDto::fromArray($tag->toArray());
try {
if ($dto->isV5()) {
$msg = __('Tag ID "{0}" is already V5', $tag->id);
throw new InternalErrorException($msg);
}
if ($tag->is_shared) {
$this->migrateShared($dto, $tag);
} else {
$this->migratePersonal($dto, $tag);
}
$this->addMigrated($tag);
} catch (Exception $e) {
// Continue with next resource if any error
$error = ['tag_id' => $tag->id, 'error_message' => $e->getMessage()];
if (Configure::read('debug')) {
$error['trace'] = $e->getTraceAsString();
}
$this->addError($error);
}
}View on GitHub (pinned to 31c1bbc10f)