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

  1. Skip or filter tags already in V5 format before migrating (query tags where metadata IS NULL / v4 only).
  2. Make migrate idempotent: catch this condition per-tag, log it, and continue with remaining tags.
  3. Run the migration single-threaded to avoid concurrent double-processing.
  4. 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

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


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)