{"record":{"id":"e0ac239b98c15508","repo":"halo-dev/halo","slug":"target-parent-has-a-cyclic-parent-chain","errorCode":null,"errorMessage":"Target parent has a cyclic parent chain.","messagePattern":"Target parent has a cyclic parent chain\\.","errorType":"validation","errorClass":"ServerWebInputException","httpStatus":400,"severity":"error","filePath":"application/src/main/java/run/halo/app/core/endpoint/console/CategoryConsoleService.java","lineNumber":223,"sourceCode":"                .map(Category.CategorySpec::getPriority)\n                .orElse(0);\n    }\n\n    private static String parentNameOf(Category category) {\n        return normalize(Optional.ofNullable(category.getSpec())\n                .map(Category.CategorySpec::getParent)\n                .orElse(null));\n    }\n\n    private static boolean isDescendant(String candidateName, String ancestorName, Map<String, Category> categoryMap) {\n        var current = candidateName;\n        var visited = new HashSet<String>();\n        while (current != null) {\n            if (Objects.equals(current, ancestorName)) {\n                return true;\n            }\n            if (!visited.add(current)) {\n                throw new ServerWebInputException(\"Target parent has a cyclic parent chain.\");\n            }\n            current = Optional.ofNullable(categoryMap.get(current))\n                    .map(CategoryConsoleService::parentNameOf)\n                    .orElse(null);\n        }\n        return false;\n    }\n\n    private static List<Category> siblings(List<Category> categories, String parentName, String excludingName) {\n        return categories.stream()\n                .filter(category -> !Objects.equals(category.getMetadata().getName(), excludingName))\n                .filter(category -> Objects.equals(parentNameOf(category), parentName))\n                .sorted(defaultCategoryComparator())\n                .collect(Collectors.toCollection(ArrayList::new));\n    }\n\n    private static int indexOf(List<Category> categories, String name) {\n        for (int i = 0; i < categories.size(); i++) {","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/halo-dev/halo/blob/d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8/application/src/main/java/run/halo/app/core/endpoint/console/CategoryConsoleService.java#L205-L241","documentation":"CategoryConsoleService.isDescendant walks a category's parent chain to decide whether a candidate parent is already an ancestor (which would create a cycle when reparenting). It records each visited name in a HashSet and throws ServerWebInputException (HTTP 400) 'Target parent has a cyclic parent chain.' if it revisits a name — i.e. the existing hierarchy already contains a loop.","triggerScenarios":"Attempting to set a category's parent such that the walk from the candidate hits a node already in visited, because the current category tree already contains a cycle (e.g. A→B→A created by a bug or direct DB edit), or reparenting a category under one of its own descendants.","commonSituations":"Data corruption from a prior buggy reparent operation; concurrent reparent requests racing and creating a loop; manual edits to category spec.parent in the database.","solutions":["Fix the existing loop in the category tree first (inspect each category's spec.parent and break the cycle).","Avoid reparenting a category under one of its own descendants.","Serialize category hierarchy mutations to prevent racing requests from forming a cycle.","Add a repair/admin script that detects and breaks cycles before allowing further reparenting."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before reparenting, ensure the new parent is not a descendant of the category:\nif (isDescendant(newParentName, category.getMetadata().getName(), categoryMap)) {\n    return Mono.error(new ServerWebInputException(\"Target parent has a cyclic parent chain.\"));\n}","typeGuard":"static boolean hasNoCycle(Map<String, Category> map, String start) {\n    var visited = new HashSet<String>();\n    var cur = start;\n    while (cur != null) {\n        if (!visited.add(cur)) return false;\n        cur = Optional.ofNullable(map.get(cur))\n            .map(CategoryConsoleService::parentNameOf).orElse(null);\n    }\n    return true;\n}","tryCatchPattern":null,"preventionTips":["Serialize category hierarchy mutations to prevent races forming cycles.","Never reparent a category under its own descendant.","Run a periodic/admin check for parent-chain cycles and repair them."],"tags":["validation","category","hierarchy","cycle-detection","http-400","data-integrity"],"backgroundTag":null,"analyzedSha":"d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8","analyzedAt":"2026-08-14T00:18:38.915Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}