{"record":{"id":"c6e4d267fc3541d7","repo":"theonedev/onedev","slug":"specified-issues-already-linked-via-specified-link","errorCode":null,"errorMessage":"Specified issues already linked via specified link spec","messagePattern":"Specified issues already linked via specified link spec","errorType":"validation","errorClass":"ValidationException","httpStatus":null,"severity":"error","filePath":"server-core/src/main/java/io/onedev/server/model/IssueLink.java","lineNumber":108,"sourceCode":"\t\t\t\t\t.anyMatch(it -> it.getSpec().equals(getSpec()) && it.getTarget().equals(getTarget())))\n\t\t\t\tthrow new ValidationException(\"Source issue already linked to target issue via specified link spec\");\n\t\t\tif (!getSpec().isMultiple()\n\t\t\t\t\t&& getSource().getTargetLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))\n\t\t\t\tthrow new ValidationException(\n\t\t\t\t\t\t\"Link spec is not multiple and the source issue is already linked to another issue via this link spec\");\n\t\t\tif (!getSpec().getParsedIssueQuery(getSource().getProject()).matches(getTarget()))\n\t\t\t\tthrow new ValidationException(\"Link spec not allowed to link to the target issue\");\n\t\t\tif (!getSpec().getOpposite().isMultiple()\n\t\t\t\t\t&& getTarget().getSourceLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))\n\t\t\t\tthrow new ValidationException(\n\t\t\t\t\t\t\"Opposite side of link spec is not multiple and the target issue is already linked to another issue via this link spec\");\n\t\t\tif (!getSpec().getOpposite().getParsedIssueQuery(getSource().getProject())\n\t\t\t\t\t.matches(getSource()))\n\t\t\t\tthrow new ValidationException(\"Opposite side of link spec not allowed to link to the source issue\");\n\t\t} else {\n\t\t\tif (getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())\n\t\t\t\t\t&& it.getLinked(getSource()).equals(getTarget())))\n\t\t\t\tthrow new ValidationException(\"Specified issues already linked via specified link spec\");\n\t\t\tif (!getSpec().isMultiple()\n\t\t\t\t\t&& getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))\n\t\t\t\tthrow new ValidationException(\n\t\t\t\t\t\t\"Link spec is not multiple and source issue is already linked to another issue via this link spec\");\n\t\t\tvar parsedIssueQuery = getSpec().getParsedIssueQuery(getSource().getProject());\n\t\t\tif (!parsedIssueQuery.matches(getSource()) || !parsedIssueQuery.matches(getTarget()))\n\t\t\t\tthrow new ValidationException(\"Link spec not allowed to link specified issues\");\n\t\t}\n\t}\n}\n","sourceCodeStart":90,"sourceCodeEnd":119,"githubUrl":"https://github.com/theonedev/onedev/blob/d44925c47c37992c828ea673a5f9620539bc3ff2/server-core/src/main/java/io/onedev/server/model/IssueLink.java#L90-L119","documentation":"In the branch where the spec has no opposite (unidirectional link spec), validate() first rejects exact duplicates: if the source issue's links already contain a link of the same spec pointing at the same target, creating it again would be redundant, so this ValidationException is thrown.","triggerScenarios":"createLink called where getSpec().getOpposite() == null and getSource().getLinks() already has a link with equal spec whose getLinked(source) equals the requested target.","commonSituations":"User double-clicks the 'link issue' button; a REST script retries a failed request that actually succeeded (no idempotency); bulk import runs twice over the same data.","solutions":["Skip creating the link if it already exists — check getLinks() for the spec/target pair first.","Make import/automation scripts idempotent by looking up the existing IssueLink before insertion.","Catch ValidationException and treat 'already linked' as success in retry logic."],"exampleFix":"// before\nIssueLink.createLink(link);\n// after\nboolean exists = src.getLinks().stream().anyMatch(l -> l.getSpec().equals(spec) && l.getLinked(src).equals(tgt));\nif (!exists) IssueLink.createLink(link);","handlingStrategy":"validation","validationCode":"boolean exists = source.getLinks().stream().anyMatch(l -> l.getSpec().equals(spec) && l.getLinked(source).equals(target));\nif (exists) return; // already linked, nothing to do","typeGuard":null,"tryCatchPattern":"try { IssueLink.createLink(link); } catch (ValidationException e) { if (e.getMessage().contains(\"already linked\")) { /* treat as success */ } else { throw e; } }","preventionTips":["Always check for an existing link before creating","Use idempotent create-or-get logic in scripts and REST calls","Debounce/duplicate-protect the link button in custom UIs"],"tags":["issue-links","duplicate","idempotency"],"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"}