{"record":{"id":"2ecd1be198929cac","repo":"theonedev/onedev","slug":"pull-request-already-closed","errorCode":null,"errorMessage":"Pull request already closed","messagePattern":"Pull request already closed","errorType":"validation","errorClass":"NotAcceptableException","httpStatus":400,"severity":"error","filePath":"server-core/src/main/java/io/onedev/server/service/impl/DefaultPullRequestService.java","lineNumber":337,"sourceCode":"\n\t\tPullRequestChange change = new PullRequestChange();\n\t\tchange.setData(new PullRequestReopenData());\n\t\tchange.setRequest(request);\n\t\tchange.setUser(user);\n\t\tchangeService.create(change, note);\n\n\t\tMergePreview mergePreview = request.checkMergePreview();\n\t\tif (mergePreview != null)\n\t\t\tupdateMergePreviewRef(request);\n\n\t\tcheckAsync(request, false, true);\n\t}\n\n\t@Transactional\n\t@Override\n \tpublic void discard(User user, PullRequest request, String note) {\n        if (!request.isOpen())\n            throw new NotAcceptableException(\"Pull request already closed\");\n\n\t\trequest.setStatus(Status.DISCARDED);\n\t\trequest.setCloseDate(new Date());\n\n\t\tPullRequestChange change = new PullRequestChange();\n\t\tchange.setData(new PullRequestDiscardData());\n\t\tchange.setRequest(request);\n\t\tchange.setUser(user);\n\t\tchangeService.create(change, note);\n\n\t\tpendingSuggestionApplyService.discard(null, request);\n\t}\n\n\t@Transactional\n\t@Override\n\tpublic void merge(User user, PullRequest request, @Nullable String commitMessage) {\n        var errorMessage = request.checkMergeCondition();\n        if (errorMessage != null)","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/theonedev/onedev/blob/d44925c47c37992c828ea673a5f9620539bc3ff2/server-core/src/main/java/io/onedev/server/service/impl/DefaultPullRequestService.java#L319-L355","documentation":"discard throws this NotAcceptableException when the pull request is not open — it is already merged, discarded, or otherwise closed. A PR can only transition to DISCARDED from the OPEN state, so discarding a closed PR is rejected.","triggerScenarios":"Calling DefaultPullRequestService.discard(user, request, note) when request.isOpen() is false — i.e. the PR status is MERGED, DISCARDED, or any non-open state. Also triggered via REST discard endpoint or checkAutoMerge's check path racing a concurrent close.","commonSituations":"Double-clicking / retrying a discard action; automation discarding PRs without checking state; a race where auto-merge merged the PR just before the discard call; REST scripts replaying old requests.","solutions":["Check request.isOpen() before calling discard and skip if already closed","Treat the PR as already handled — no action needed if it was merged or discarded","Handle the NotAcceptableException and read the PR's current status to decide next steps"],"exampleFix":"// before\nprService.discard(user, request, \"no longer needed\"); // 400 \"Pull request already closed\"\n// after\nif (request.isOpen())\n    prService.discard(user, request, \"no longer needed\");","handlingStrategy":"validation","validationCode":"boolean canDiscard = request.isOpen();","typeGuard":null,"tryCatchPattern":"try {\n    prService.discard(user, request, note);\n} catch (NotAcceptableException e) {\n    // PR already merged/discarded; no-op\n}","preventionTips":["Always gate discard actions on request.isOpen()","Disable discard buttons for closed PRs in UI","Avoid replaying old discard requests in scripts"],"tags":["pull-request","discard","state"],"backgroundTag":"invalid-state-transition","analyzedSha":"d44925c47c37992c828ea673a5f9620539bc3ff2","analyzedAt":"2026-09-06T07:18:27.995Z","contentChangedAt":"2026-09-06T07:18:27.995Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}