{"record":{"id":"25d2b6a955011364","repo":"plandex-ai/plandex","slug":"plan-already-archived","errorCode":null,"errorMessage":"Plan already archived","messagePattern":"Plan already archived","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"app/server/handlers/plans_changes.go","lineNumber":452,"sourceCode":"\tif auth == nil {\n\t\treturn\n\t}\n\n\tlog.Println(\"Received request for ArchivePlanHandler\")\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 already archived\")\n\t\thttp.Error(w, \"Plan already archived\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tres, err := db.Conn.Exec(\"UPDATE plans SET archived_at = NOW() 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":434,"sourceCodeEnd":470,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/handlers/plans_changes.go#L434-L470","documentation":"ArchivePlanHandler rejects archiving a plan that already has a non-nil archived_at timestamp, returning HTTP 400 'Plan already archived'. This is an intentional idempotency/state guard: a plan can only be archived once.","triggerScenarios":"POST/PUT to the archive endpoint for a planId whose plans.archived_at is already set — i.e. the plan was archived previously by this or another user/session.","commonSituations":"Double-clicking an archive button; retrying a request that actually succeeded the first time; two operators archiving the same plan concurrently; automated scripts running archive jobs repeatedly.","solutions":["Fetch the plan first and skip archiving if archivedAt is already set (treat 400 as success in idempotent flows)","Dedupe UI submissions (disable button while in flight)","Don't retry this request automatically — it's a permanent 400, not transient","If you need the plan active again, call unarchive instead"],"exampleFix":"// before\nawait archivePlan(planId); // throws on second call\n// after\nconst plan = await getPlan(planId);\nif (!plan.archivedAt) await archivePlan(planId);","handlingStrategy":"validation","validationCode":"const plan = await getPlan(planId);\nif (plan.archivedAt) { /* already archived, skip */ return; }\nawait archivePlan(planId);","typeGuard":"function isAlreadyArchived(v) { return v != null; } // plan.ArchivedAt (*time.Time) non-nil means archived\nif (isAlreadyArchived(plan.archivedAt)) skipArchive();","tryCatchPattern":"try { await archivePlan(planId); } catch (e) { if (e.status === 400 && e.message === 'Plan already archived') return; /* treat as success (idempotent) */ throw e; }","preventionTips":["Check archivedAt before calling archive","Treat 'Plan already archived' 400 as an idempotent success","Disable archive buttons for already-archived plans","Debounce/dedupe rapid archive submissions"],"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"}