{"record":{"id":"77a87971043eec2d","repo":"apache/superset","slug":"annotation-not-found","errorCode":null,"errorMessage":"Annotation not found.","messagePattern":"Annotation not found\\.","errorType":"exception","errorClass":"AnnotationNotFoundError","httpStatus":404,"severity":"error","filePath":"superset/commands/annotation_layer/annotation/delete.py","lineNumber":48,"sourceCode":"logger = logging.getLogger(__name__)\n\n\nclass DeleteAnnotationCommand(BaseCommand):\n    def __init__(self, model_ids: list[int]):\n        self._model_ids = model_ids\n        self._models: Optional[list[Annotation]] = None\n\n    @transaction(on_error=partial(on_error, reraise=AnnotationDeleteFailedError))\n    def run(self) -> None:\n        self.validate()\n        assert self._models\n        AnnotationDAO.delete(self._models)\n\n    def validate(self) -> None:\n        # Validate/populate model exists\n        self._models = AnnotationDAO.find_by_ids(self._model_ids)\n        if not self._models or len(self._models) != len(self._model_ids):\n            raise AnnotationNotFoundError()\n","sourceCodeStart":30,"sourceCodeEnd":49,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/commands/annotation_layer/annotation/delete.py#L30-L49","documentation":"AnnotationNotFoundError raised by DeleteAnnotationCommand.validate() (delete.py:48) when AnnotationDAO.find_by_ids returns fewer models than requested ids — at least one annotation id in the DELETE payload does not exist. The delete command accepts a list (bulk delete), so a single stale id fails the whole batch; HTTP 404.","triggerScenarios":"DELETE /api/v1/annotation/ with body [1,2,999] where 999 is gone; double-submit of a delete request (second call finds nothing); deleting annotations already removed by another user/session.","commonSituations":"Bulk UI actions racing with another editor's deletes; retry logic re-sending an already-applied bulk delete; idempotent scripts assuming re-delete succeeds.","solutions":["Retry the delete with only the ids that still exist (filter via GET /api/v1/annotation/?q=(id:in:(...)) first).","Make client delete actions idempotent: treat 404 on delete as success.","Avoid double-submission in the UI (disable button while the request is in flight)."],"exampleFix":"# before\nDeleteAnnotationCommand([1, 2, 999]).run()  # 999 already deleted -> whole batch 404s\n\n# after\nexisting = AnnotationDAO.find_by_ids([1, 2, 999])\nDeleteAnnotationCommand([m.id for m in existing]).run()","handlingStrategy":"validation","validationCode":"from superset.daos.annotation_layer import AnnotationDAO\n\nexisting = AnnotationDAO.find_by_ids(model_ids)\nlive_ids = [m.id for m in existing]\nif len(live_ids) != len(model_ids):\n    model_ids = live_ids  # delete only what still exists","typeGuard":null,"tryCatchPattern":"try:\n    DeleteAnnotationCommand(model_ids).run()\nexcept AnnotationNotFoundError:\n    # treat as success: already deleted elsewhere\n    log.info(\"annotations already deleted: %s\", model_ids)","preventionTips":["Treat 404 on bulk delete as idempotent success in clients.","Filter requested ids through a live existence check before issuing the bulk DELETE.","Guard the UI against double-submit of delete actions."],"tags":["annotation","not-found","bulk-delete","race-condition"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}