{"record":{"id":"2a72944297d02528","repo":"spring-projects/spring-security","slug":"exception-thrown-because-of-a-cycle-in-the-role-hi","errorCode":null,"errorMessage":"Exception thrown because of a cycle in the role hierarchy definition!","messagePattern":"Exception thrown because of a cycle in the role hierarchy definition!","errorType":"exception","errorClass":"CycleInRoleHierarchyException","httpStatus":null,"severity":"critical","filePath":"core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java","lineNumber":221,"sourceCode":"\t * CycleInRoleHierarchyException if a cycle in the role hierarchy definition is\n\t * detected)\n\t */\n\tprivate static Map<String, Set<GrantedAuthority>> buildRolesReachableInOneOrMoreStepsMap(\n\t\t\tMap<String, Set<GrantedAuthority>> hierarchy) {\n\t\tMap<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = new HashMap<>();\n\t\t// iterate over all higher roles from rolesReachableInOneStepMap\n\t\tfor (String roleName : hierarchy.keySet()) {\n\t\t\tSet<GrantedAuthority> rolesToVisitSet = new HashSet<>(hierarchy.get(roleName));\n\t\t\tSet<GrantedAuthority> visitedRolesSet = new HashSet<>();\n\t\t\twhile (!rolesToVisitSet.isEmpty()) {\n\t\t\t\t// take a role from the rolesToVisit set\n\t\t\t\tGrantedAuthority lowerRole = rolesToVisitSet.iterator().next();\n\t\t\t\trolesToVisitSet.remove(lowerRole);\n\t\t\t\tif (!visitedRolesSet.add(lowerRole) || !hierarchy.containsKey(lowerRole.getAuthority())) {\n\t\t\t\t\tcontinue; // Already visited role or role with missing hierarchy\n\t\t\t\t}\n\t\t\t\telse if (roleName.equals(lowerRole.getAuthority())) {\n\t\t\t\t\tthrow new CycleInRoleHierarchyException();\n\t\t\t\t}\n\t\t\t\trolesToVisitSet.addAll(hierarchy.get(lowerRole.getAuthority()));\n\t\t\t}\n\t\t\trolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);\n\t\t\tlogger.debug(LogMessage.format(\n\t\t\t\t\t\"buildRolesReachableInOneOrMoreStepsMap() - From role %s one can reach %s in one or more steps.\",\n\t\t\t\t\troleName, visitedRolesSet));\n\t\t}\n\t\treturn rolesReachableInOneOrMoreStepsMap;\n\t}\n\n\t/**\n\t * Builder class for constructing a {@link RoleHierarchyImpl} based on a hierarchical\n\t * role structure.\n\t *\n\t * @author Federico Herrera\n\t * @since 6.3\n\t */","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java#L203-L239","documentation":"RoleHierarchyImpl.buildRolesReachableInOneOrMoreStepsMap expands each role through the hierarchy; while walking reachable roles it detects when a role reaches itself, meaning the hierarchy definition contains a cycle (e.g. ROLE_A > ROLE_B > ROLE_A). It throws CycleInRoleHierarchyException at construction time, so the application fails to start.","triggerScenarios":"Defining a role hierarchy string where roles reference each other in a loop, directly (ROLE_A > ROLE_A) or transitively (ROLE_A > ROLE_B > ROLE_C > ROLE_A), typically via setHierarchy(...) or the role-hierarchy configuration property.","commonSituations":"Copy-pasted or merged hierarchy definitions in application.yml/security config; programmatically built hierarchies appending mappings that close a loop; multi-module setups where two modules each add inverse mappings.","solutions":["Remove the cyclic mapping from the hierarchy string so roles form a DAG (each role reachable only 'downward')","Log/print the hierarchy at startup and trace the loop among the roles named in the exception","Refactor mutually dependent roles into a common parent role instead of circular references","Add a unit test calling new RoleHierarchyImpl(hierarchyString) for every shipped hierarchy to fail fast on cycles"],"exampleFix":"// before\nString hierarchy = \"ROLE_A > ROLE_B\\nROLE_B > ROLE_C\\nROLE_C > ROLE_A\"; // cycle\n// after\nString hierarchy = \"ROLE_A > ROLE_B\\nROLE_A > ROLE_C\";","handlingStrategy":"validation","validationCode":"Set<String> seen = new HashSet<>();\nfor (String[] line : mappings) { String from = line[0], to = line[1];\n  if (from.equals(to) || createsBackEdge(from, to)) throw new IllegalArgumentException(\"cycle in role hierarchy: \" + from + \" -> \" + to); }","typeGuard":"null","tryCatchPattern":"try { new RoleHierarchyImpl(hierarchyString); }\ncatch (CycleInRoleHierarchyException e) { failFast(\"Fix role hierarchy cycle before startup\"); }","preventionTips":["Unit-test every hierarchy string at build time","Keep role edges strictly one-directional (senior > junior)","Reject self-references when building hierarchies programmatically","Review merged hierarchy config from multiple modules for loops"],"tags":["role-hierarchy","configuration","startup-failure","cycle"],"backgroundTag":"invalid-config-value","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}