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
- Fetch the key first and skip the update if isExpired() is already true
- Make the expiry operation idempotent in the caller (early-return on already-expired)
- 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
- Make expiry operations idempotent (skip when already expired)
- Guard concurrent rotation jobs with locks/unique scheduling
- Avoid replaying update requests without re-checking state
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
- Folder ID " " is already V5
- The metadata key is already shared with the user.
- The metadata session key data is identical.
- The request data is invalid.
- The request is already completed.
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)