passbolt/passbolt_api · error · BadRequestException

The metadata key is already marked as expired.

Error message

The metadata key is already marked as expired.

What it means

MetadataKeyUpdateService::update() throws a BadRequestException when the metadata key already has an expired timestamp set. Expiry is terminal in the same way deletion is: the service forbids re-patching an already expired key via update().

Solutions

  1. Fetch the key first and skip the update if isExpired() is already true
  2. Make the expiry operation idempotent in the caller (early-return on already-expired)
  3. Deduplicate concurrent requests (locking or unique job scheduling) in rotation jobs

Example fix

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

Strategy: validation

Validate before calling

$key = $metadataKeysTable->get($keyId);
if ($key->isExpired()) {
    return; // already expired, nothing to do
}

Type guard

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

Try / catch

try {
    $service->update($uac, $keyId, $dto);
} catch (BadRequestException $e) {
    // key already expired: treat as success in idempotent jobs
}

Prevention

When it happens

Trigger: Calling update() with an expired/datetime patch on a key whose expired field is already non-null (isExpired() === true).

Common situations: Two admins concurrently mark the same key expired; a scheduled rotation job re-runs and tries to expire the key twice; replayed HTTP requests.

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/cdef25b9b8fee33b. Report an issue: GitHub.

Appendix: source

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

            /** @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,
            'modified_by' => $uac->getId(),
        ];
        /** @var \Passbolt\Metadata\Model\Entity\MetadataKey $metadataKey */
        $metadataKey = $metadataKeysTable->patchEntity($metadataKey, $patch, $options);
        if ($metadataKey->getErrors()) {

View on GitHub (pinned to 31c1bbc10f)