passbolt/passbolt_api · error · InternalErrorException

Folder ID " " is already V5

Error message

Folder ID "{0}" is already V5

What it means

During bulk V4-to-V5 folder migration, each folder is converted to a MetadataFolderDto and checked; if the DTO already reports isV5() (metadata populated, name nulled), an InternalErrorException is thrown because migrating an already-migrated folder is an invariant violation in the batch. The message includes the folder ID to locate the offender.

Solutions

  1. Exclude already-v5 folders from the query (add a condition on metadata IS NOT NULL / name IS NULL).
  2. Skip-and-log already-migrated folders instead of throwing, to make the migration idempotent.
  3. Re-run the migration only after confirming no partial state: check folders where metadata is set but name is not null.
  4. Ensure only one migration job runs at a time (use a lock) to avoid double processing.

Example fix

// before: select all folders, throw on already-v5
$folders = $foldersTable->find()->contain('Permissions')->all();
// after: filter at query time
$folders = $foldersTable->find()
    ->where(['metadata IS' => null])
    ->contain('Permissions')->all();
Defensive patterns

Strategy: validation

Validate before calling

$remaining = TableRegistry::getTableLocator()->get('Folders')
    ->find()->where(['metadata IS' => null])->count();
if ($remaining === 0) { /* nothing to migrate, skip the job */ }

Type guard

if ($dto->isV5()) { continue; } // skip already-migrated folders

Try / catch

try {
    $service->migrate($uac);
} catch (\Cake\Http\Exception\InternalErrorException $e) {
    if (str_contains($e->getMessage(), 'is already V5')) {
        // exclude that folder id and resume migration
    }
}

Prevention

When it happens

Trigger: Running the migrate-all-folders command/service on a database where some folders were already migrated to v5 (partially completed earlier run, or folders migrated manually/via another job) — the query selecting candidate folders matched them again.

Common situations: Re-running a failed migration job without cleaning up; concurrent migration workers double-processing folders; snapshot/restore mixing pre- and post-migration data.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/6a6e5c4a4929252b. Report an issue: GitHub.

Appendix: source

Thrown at plugins/PassboltCe/Metadata/src/Service/Migration/MigrateAllV4FoldersToV5Service.php:100

            ->find()
            ->contain(['Permissions.Users.Gpgkeys'])
            ->where(['name IS NOT NULL'])
            ->all()
            ->toArray();

        if (empty($folders)) {
            $this->addError(['error_message' => __('No folders to migrate.')]);

            return $this->getResult();
        }

        foreach ($folders as $folder) {
            $dto = MetadataFolderDto::fromArray($folder->toArray());

            try {
                if ($dto->isV5()) {
                    $msg = __('Folder ID "{0}" is already V5', $folder->id);
                    throw new InternalErrorException($msg);
                }
                if (count($folder->permissions) === 0) {
                    $msg = __('No permission found for folder ID {0}', $folder->id);
                    throw new InternalErrorException($msg);
                }

                if (count($folder->permissions) === 1) {
                    $this->migratePersonal($dto, $folder);
                } else {
                    $this->migrateShared($dto, $folder);
                }
                $this->addMigrated($folder);
            } catch (Exception $e) {
                // Continue with next resource if any error
                $error = ['folder_id' => $folder->id, 'error_message' => $e->getMessage()];
                if (Configure::read('debug')) {
                    $error['trace'] = $e->getTraceAsString();
                }

View on GitHub (pinned to 31c1bbc10f)