{"record":{"id":"634a1ca1ea389e6e","repo":"passbolt/passbolt_api","slug":"the-metadata-key-has-already-been-deleted","errorCode":null,"errorMessage":"The metadata key has already been deleted.","messagePattern":"The metadata key has already been deleted\\.","errorType":"http","errorClass":"NotFoundException","httpStatus":404,"severity":"warning","filePath":"plugins/PassboltCe/Metadata/src/Service/MetadataKey/MetadataKeyDeleteService.php","lineNumber":66,"sourceCode":"        $uac->assertIsAdmin();\n\n        if (!Validation::uuid($id)) {\n            throw new BadRequestException(__('The metadata key ID should be a valid UUID.'));\n        }\n\n        $metadataKeysTable = $this->fetchTable('Passbolt/Metadata.MetadataKeys');\n\n        // Assert the key exist\n        try {\n            /** @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 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 previously marked as expired\n        if (!$metadataKey->isExpired()) {\n            throw new BadRequestException(__('The metadata key should be marked as expired first.'));\n        }\n\n        // Assert the key is not used by folders, resources, tags, etc.\n        if ((new MetadataKeyAssertUsageService())->isKeyInUse($metadataKey->get('id'))) {\n            $msg = __('The metadata key is still in use, migrate the remaining items to the new key first.');\n            throw new BadRequestException($msg);\n        }\n\n        // Patch the key deleted field with the current time\n        $options['accessibleFields'] = ['deleted' => true, 'modified_by' => true];\n        $patch = ['deleted' => DateTime::now(), 'modified_by' => $uac->getId()];\n        $metadataKey = $metadataKeysTable->patchEntity($metadataKey, $patch, $options);\n        if ($metadataKey->getErrors()) {","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/Metadata/src/Service/MetadataKey/MetadataKeyDeleteService.php#L48-L84","documentation":"Thrown when the targeted metadata key already has its 'deleted' flag set, i.e. a previous delete already succeeded. The service rejects the redundant delete with NotFoundException (HTTP 404) to keep delete idempotent-unfriendly and explicit.","triggerScenarios":"DELETE /metadata/keys/<id> executed twice; a retry after a timed-out-but-successful first delete; another admin deleted the key between the caller's fetch and the delete call.","commonSituations":"Automated scripts or CI pipelines that re-run a migration step without checking current key state; double-click / duplicate form submission in tooling built on the API.","solutions":["Check the key's deleted flag (or re-fetch the key) before calling delete","Treat 404 with this message as 'already done' and skip rather than fail","Avoid retrying delete blindly on timeout — first query the key state"],"exampleFix":"// before\nawait deleteMetadataKey(id);\nawait deleteMetadataKey(id); // second call throws\n// after\nif (!isKeyDeleted(id)) {\n  await deleteMetadataKey(id);\n}","handlingStrategy":"validation","validationCode":"const key = await getMetadataKey(id);\nif (key.deleted) return; // already deleted, skip","typeGuard":null,"tryCatchPattern":"try {\n  await deleteMetadataKey(id);\n} catch (e) {\n  if (e.status === 404 && /already been deleted/.test(e.message)) return;\n  throw e;\n}","preventionTips":["Make delete steps idempotent in scripts by checking deleted flag first","Don't blindly retry delete on timeout — verify state first"],"tags":["idempotency","metadata-keys","not-found"],"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-21T04:17:39.646Z"}