halo-dev/halo · error · ServerWebInputException
Target parent has a cyclic parent chain.
Error message
Target parent has a cyclic parent chain.
What it means
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.
Source
Thrown at application/src/main/java/run/halo/app/core/endpoint/console/CategoryConsoleService.java:223
.map(Category.CategorySpec::getPriority)
.orElse(0);
}
private static String parentNameOf(Category category) {
return normalize(Optional.ofNullable(category.getSpec())
.map(Category.CategorySpec::getParent)
.orElse(null));
}
private static boolean isDescendant(String candidateName, String ancestorName, Map<String, Category> categoryMap) {
var current = candidateName;
var visited = new HashSet<String>();
while (current != null) {
if (Objects.equals(current, ancestorName)) {
return true;
}
if (!visited.add(current)) {
throw new ServerWebInputException("Target parent has a cyclic parent chain.");
}
current = Optional.ofNullable(categoryMap.get(current))
.map(CategoryConsoleService::parentNameOf)
.orElse(null);
}
return false;
}
private static List<Category> siblings(List<Category> categories, String parentName, String excludingName) {
return categories.stream()
.filter(category -> !Objects.equals(category.getMetadata().getName(), excludingName))
.filter(category -> Objects.equals(parentNameOf(category), parentName))
.sorted(defaultCategoryComparator())
.collect(Collectors.toCollection(ArrayList::new));
}
private static int indexOf(List<Category> categories, String name) {
for (int i = 0; i < categories.size(); i++) {View on GitHub (pinned to d2f5165f9c)
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.
Defensive patterns
Strategy: validation
Validate before calling
// Before reparenting, ensure the new parent is not a descendant of the category:
if (isDescendant(newParentName, category.getMetadata().getName(), categoryMap)) {
return Mono.error(new ServerWebInputException("Target parent has a cyclic parent chain."));
} Type guard
static boolean hasNoCycle(Map<String, Category> map, String start) {
var visited = new HashSet<String>();
var cur = start;
while (cur != null) {
if (!visited.add(cur)) return false;
cur = Optional.ofNullable(map.get(cur))
.map(CategoryConsoleService::parentNameOf).orElse(null);
}
return true;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Target parent has a cyclic parent chain.
- The required parameter 'commentName' is missing.
- urls must not be empty.
- url must not be blank.
- Unsupported URL protocol: {}
AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14).
Data as JSON: /api/errors/e0ac239b98c15508.
Report an issue: GitHub.