{"record":{"id":"cdef25b9b8fee33b","repo":"passbolt/passbolt_api","slug":"the-metadata-key-is-already-marked-as-expired","errorCode":null,"errorMessage":"The metadata key is already marked as expired.","messagePattern":"The metadata key is already marked as expired\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltCe/Metadata/src/Service/MetadataKey/MetadataKeyUpdateService.php","lineNumber":77,"sourceCode":"            /** @var \\Passbolt\\Metadata\\Model\\Entity\\MetadataKey $metadataKey */\n            $metadataKey = $metadataKeysTable->get($id);\n        } catch (RecordNotFoundException $exception) { // @phpstan-ignore-line\n            throw new NotFoundException(__('The metadata key does not exist or has been deleted.'), 404, $exception);\n        }\n\n        // Assert fingerprint is the same\n        if ($metadataKey->fingerprint !== $dto->fingerprint) {\n            throw new NotFoundException(__('The metadata key fingerprint is invalid.'));\n        }\n\n        // Assert the key is not already deleted\n        if ($metadataKey->isDeleted()) {\n            throw new NotFoundException(__('The metadata key has already been deleted.'));\n        }\n\n        // Assert they key was not previously marked as expired\n        if ($metadataKey->isExpired()) {\n            throw new BadRequestException(__('The metadata key is already marked as expired.'));\n        }\n\n        // Patch the key deleted field with the current time\n        $options = [\n            'accessibleFields' => [\n                'fingerprint' => true, 'armored_key' => true, 'expired' => true, 'modified_by' => true,\n            ],\n            'validate' => 'update',\n        ];\n        $patch = [\n            'fingerprint' => $dto->fingerprint,\n            'armored_key' => $dto->armoredKey,\n            'expired' => $dto->expired,\n            'modified_by' => $uac->getId(),\n        ];\n        /** @var \\Passbolt\\Metadata\\Model\\Entity\\MetadataKey $metadataKey */\n        $metadataKey = $metadataKeysTable->patchEntity($metadataKey, $patch, $options);\n        if ($metadataKey->getErrors()) {","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/Metadata/src/Service/MetadataKey/MetadataKeyUpdateService.php#L59-L95","documentation":"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().","triggerScenarios":"Calling update() with an expired/datetime patch on a key whose expired field is already non-null (isExpired() === true).","commonSituations":"Two admins concurrently mark the same key expired; a scheduled rotation job re-runs and tries to expire the key twice; replayed HTTP requests.","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"],"exampleFix":"// before\n$service->update($uac, $keyId, $dto);\n// after\n$key = $keysTable->get($keyId);\nif (!$key->isExpired()) {\n    $service->update($uac, $keyId, $dto);\n}","handlingStrategy":"validation","validationCode":"$key = $metadataKeysTable->get($keyId);\nif ($key->isExpired()) {\n    return; // already expired, nothing to do\n}","typeGuard":"$canExpire = fn (MetadataKey $k): bool => !$k->isExpired() && !$k->isDeleted();","tryCatchPattern":"try {\n    $service->update($uac, $keyId, $dto);\n} catch (BadRequestException $e) {\n    // key already expired: treat as success in idempotent jobs\n}","preventionTips":["Make expiry operations idempotent (skip when already expired)","Guard concurrent rotation jobs with locks/unique scheduling","Avoid replaying update requests without re-checking state"],"tags":["metadata","bad-request","idempotency"],"backgroundTag":"invalid-state-transition","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T09:17:21.228Z"}