{"record":{"id":"8d9bd69788ce3a63","repo":"plandex-ai/plandex","slug":"plan-isn-t-archived","errorCode":null,"errorMessage":"Plan isn't archived","messagePattern":"Plan isn't archived","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"app/server/handlers/plans_changes.go","lineNumber":500,"sourceCode":"\tif auth == nil {\n\t\treturn\n\t}\n\n\tlog.Println(\"Received request for UnarchivePlanHandler\")\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlanArchive(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tif plan.ArchivedAt == nil {\n\t\tlog.Println(\"Plan isn't archived\")\n\t\thttp.Error(w, \"Plan isn't archived\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tres, err := db.Conn.Exec(\"UPDATE plans SET archived_at = NULL WHERE id = $1\", planId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error archiving plan: %v\\n\", err)\n\t\thttp.Error(w, \"Error archiving plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\trowsAffected, err := res.RowsAffected()\n\tif err != nil {\n\t\tlog.Printf(\"Error getting rows affected: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting rows affected: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n","sourceCodeStart":482,"sourceCodeEnd":518,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/handlers/plans_changes.go#L482-L518","documentation":"UnarchivePlanHandler rejects unarchiving a plan whose archived_at is NULL, returning HTTP 400 \"Plan isn't archived\". It's a state guard ensuring unarchive only applies to plans that are currently archived.","triggerScenarios":"Calling the unarchive endpoint for a planId whose archived_at is NULL — the plan was never archived or was already unarchived.","commonSituations":"Double-clicking an unarchive button; concurrent unarchive by another session; retrying a request that already succeeded; scripts that unarchive unconditionally on startup.","solutions":["Fetch the plan first and skip unarchive if archivedAt is null (treat 400 as already-done)","Disable the unarchive action in the UI for non-archived plans","Don't auto-retry this 400 — it's a permanent state conflict","If unarchiving is genuinely needed, it means state changed elsewhere; refresh plan data"],"exampleFix":"// before\nawait unarchivePlan(planId);\n// after\nconst plan = await getPlan(planId);\nif (plan.archivedAt) await unarchivePlan(planId);","handlingStrategy":"validation","validationCode":"const plan = await getPlan(planId);\nif (!plan.archivedAt) { /* not archived, nothing to unarchive */ return; }\nawait unarchivePlan(planId);","typeGuard":"function isArchived(v) { return v != null; } // archivedAt non-null means the plan is archived\nif (!isArchived(plan.archivedAt)) skipUnarchive();","tryCatchPattern":"try { await unarchivePlan(planId); } catch (e) { if (e.status === 400 && e.message === \"Plan isn't archived\") return; /* treat as success */ throw e; }","preventionTips":["Check archivedAt before calling unarchive","Only expose unarchive in the UI for archived plans","Treat the 400 as an idempotent no-op in scripts","Refresh plan state after any archive/unarchive to avoid stale guards"],"tags":["http","bad-request","state-conflict","go"],"backgroundTag":"invalid-state-transition","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}