halo-dev/halo · error · ServerWebInputException

Target parent has a cyclic parent chain.

Error message

Target parent has a cyclic parent chain.

What it means

MenuItemConsoleService.isDescendant performs the same visited-set cycle detection as the category service, but for menu items (walking parent menu items via parentNameOf). It throws ServerWebInputException (HTTP 400) 'Target parent has a cyclic parent chain.' when the menu item hierarchy already loops, to prevent reparenting an item under its own descendant or operating on already-corrupted data.

Source

Thrown at application/src/main/java/run/halo/app/core/endpoint/console/MenuItemConsoleService.java:242

                .map(MenuItem.MenuItemSpec::getParent)
                .orElse(null));
    }

    private static String menuNameOf(MenuItem menuItem) {
        return Optional.ofNullable(menuItem.getSpec())
                .map(MenuItem.MenuItemSpec::getMenuName)
                .orElse(null);
    }

    private static boolean isDescendant(String candidateName, String ancestorName, Map<String, MenuItem> itemMap) {
        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(itemMap.get(current))
                    .map(MenuItemConsoleService::parentNameOf)
                    .orElse(null);
        }
        return false;
    }

    private static List<MenuItem> siblings(List<MenuItem> items, String parentName, String excludingName) {
        return items.stream()
                .filter(item -> !Objects.equals(item.getMetadata().getName(), excludingName))
                .filter(item -> Objects.equals(parentNameOf(item), parentName))
                .sorted(defaultMenuItemComparator())
                .collect(Collectors.toCollection(ArrayList::new));
    }

    private static int indexOf(List<MenuItem> menuItems, String name) {
        for (int i = 0; i < menuItems.size(); i++) {

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Inspect and repair the menu item hierarchy to remove any existing loop.
  2. Do not reparent an item under one of its descendants.
  3. Serialize menu hierarchy edits to avoid races forming cycles.
  4. Validate imported menu data for cycles before applying.
Defensive patterns

Strategy: validation

Validate before calling

if (isDescendant(newParentName, item.getMetadata().getName(), itemMap)) {
    return Mono.error(new ServerWebInputException("Target parent has a cyclic parent chain."));
}

Type guard

static boolean hasNoMenuItemCycle(Map<String, MenuItem> 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(MenuItemConsoleService::parentNameOf).orElse(null);
    }
    return true;
}

Prevention

When it happens

Trigger: Reparenting a menu item such that the candidate parent chain revisits a node; operating on a menu whose items already form a loop due to prior corruption or racing writes.

Common situations: Concurrent menu restructuring requests racing; direct edits to menu item spec parent/priority creating a loop; import of menu data that contained a cycle.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/997603f8ae12b043. Report an issue: GitHub.