passbolt/passbolt_api · error · NotFoundException

The metadata key has already been deleted.

Error message

The metadata key has already been deleted.

What it means

MetadataKeyUpdateService::update() throws a NotFoundException when the target metadata key's deleted flag is set. Once a key is soft-deleted it is immutable, so any further update (e.g. marking it expired) is refused. The 404-style message intentionally hides the deleted state distinction from unauthorized callers.

Solutions

  1. Check the key's deleted flag (GET /metadata/keys) before attempting any update
  2. If the key must exist, recreate it via the metadata key create endpoint instead of updating the deleted one
  3. Remove the duplicate/stale update request from the client workflow
  4. If deletion was a mistake, restore the key's deleted field via an allowed flow rather than update()

Example fix

// before
$service->update($uac, $keyId, $dto); // throws if key deleted
// after
$key = $keysTable->get($keyId);
if (!$key->isDeleted()) {
    $service->update($uac, $keyId, $dto);
}
Defensive patterns

Strategy: validation

Validate before calling

$key = $metadataKeysTable->find()->where(['id' => $keyId])->first();
if ($key === null || $key->isDeleted()) {
    return; // skip update for deleted keys
}

Type guard

$isUpdatable = fn (MetadataKey $k): bool => !$k->isDeleted() && !$k->isExpired();

Try / catch

try {
    $service->update($uac, $keyId, $dto);
} catch (NotFoundException $e) {
    // key deleted or missing: refresh local cache of metadata keys
}

Prevention

When it happens

Trigger: Calling PUT /metadata/keys/{id} (update) on a metadata key whose entity returns isDeleted() === true, e.g. re-expiring or re-arming an already deleted key.

Common situations: Client cached a stale key id after another admin deleted the key; replaying an update request twice; UI not refreshed after a deletion.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at plugins/PassboltCe/Metadata/src/Service/MetadataKey/MetadataKeyUpdateService.php:72

        $metadataKeysTable = $this->fetchTable('Passbolt/Metadata.MetadataKeys');

        // Assert the key exist
        try {
            /** @var \Passbolt\Metadata\Model\Entity\MetadataKey $metadataKey */
            $metadataKey = $metadataKeysTable->get($id);
        } catch (RecordNotFoundException $exception) { // @phpstan-ignore-line
            throw new NotFoundException(__('The metadata key does not exist or has been deleted.'), 404, $exception);
        }

        // Assert fingerprint is the same
        if ($metadataKey->fingerprint !== $dto->fingerprint) {
            throw new NotFoundException(__('The metadata key fingerprint is invalid.'));
        }

        // Assert the key is not already deleted
        if ($metadataKey->isDeleted()) {
            throw new NotFoundException(__('The metadata key has already been deleted.'));
        }

        // Assert they key was not previously marked as expired
        if ($metadataKey->isExpired()) {
            throw new BadRequestException(__('The metadata key is already marked as expired.'));
        }

        // Patch the key deleted field with the current time
        $options = [
            'accessibleFields' => [
                'fingerprint' => true, 'armored_key' => true, 'expired' => true, 'modified_by' => true,
            ],
            'validate' => 'update',
        ];
        $patch = [
            'fingerprint' => $dto->fingerprint,
            'armored_key' => $dto->armoredKey,
            'expired' => $dto->expired,

View on GitHub (pinned to 31c1bbc10f)