{"record":{"id":"ff94f6ed2f72414a","repo":"nextcloud/server","slug":"the-calendar-object-you-re-trying-to-restore-is-no","errorCode":null,"errorMessage":"The calendar object you're trying to restore is not marked as deleted","messagePattern":"The calendar object you're trying to restore is not marked as deleted","errorType":"exception","errorClass":"BadRequest","httpStatus":400,"severity":"error","filePath":"apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php","lineNumber":58,"sourceCode":"\t\tthrow new NotImplemented();\n\t}\n\n\t#[\\Override]\n\tpublic function getChild($name) {\n\t\tif (!preg_match(\"/(\\d+)\\\\.ics/\", $name, $matches)) {\n\t\t\tthrow new NotFound();\n\t\t}\n\n\t\t$data = $this->caldavBackend->getDeletedCalendarObjectByIdForPrincipal(\n\t\t\t(int)$matches[1],\n\t\t\t$this->principalInfo['uri'],\n\t\t);\n\n\t\tif ($data === null) {\n\t\t\tthrow new NotFound();\n\t\t}\n\t\tif (!isset($data['deleted_at'])) {\n\t\t\tthrow new BadRequest('The calendar object you\\'re trying to restore is not marked as deleted');\n\t\t}\n\n\t\treturn new DeletedCalendarObject(\n\t\t\t$this->getRelativeObjectPath($data),\n\t\t\t$data,\n\t\t\t$this->principalInfo['uri'],\n\t\t\t$this->caldavBackend\n\t\t);\n\t}\n\n\t#[\\Override]\n\tpublic function createFile($name, $data = null) {\n\t\tthrow new Forbidden();\n\t}\n\n\t#[\\Override]\n\tpublic function createDirectory($name) {\n\t\tthrow new Forbidden();","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php#L40-L76","documentation":"The row was found for this principal, but its deleted_at column is unset, so the object is not actually in a deleted state; getChild() refuses to wrap it in a DeletedCalendarObject and throws BadRequest (HTTP 400). The trashbin only serves objects genuinely marked deleted, and this state mismatch typically appears when the row changed (e.g. a concurrent restore) after the client obtained its listing.","triggerScenarios":"GET/MOVE/DELETE on trashbin/objects/<id>.ics for an object whose deleted_at is NULL — for example another session restored the event between the REPORT listing and this request, or a restore/marker write raced with the trashbin read.","commonSituations":"Double-clicked restore in a UI where the first request already succeeded; two devices syncing the same trashbin; retries after a timeout that actually completed server-side.","solutions":["Re-run the calendar-query REPORT; if the id no longer appears, treat the object as restored and update the UI","Retry the original intent against the live calendar if the object was restored concurrently","If it persists for an id that the REPORT still lists, inspect oc_calendar_objects.deleted_at for that row to detect a corrupted marker"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    await client.get(href);\n} catch (e) {\n    if (e.status === 400 && /not marked as deleted/.test(e.message)) {\n        const listing = await calendarQueryReport(trashbinObjectsUrl);\n        if (listing.includes(href)) return client.get(href); // transient race\n        return; // object was restored concurrently\n    }\n    throw e;\n}","preventionTips":["Re-check the trashbin listing immediately before restore/delete of entries fetched earlier","Make restore operations idempotent by handling 'already restored' outcomes"],"tags":["caldav","trashbin","http-400","race-condition","invalid-state"],"backgroundTag":"invalid-object-state","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}