{"record":{"id":"40d35b0361a827ae","repo":"elastic/elasticsearch","slug":"circular-reference-detected","errorCode":null,"errorMessage":"circular reference detected: {}","messagePattern":"circular reference detected: (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"libs/grok/src/main/java/org/elasticsearch/grok/PatternBank.java","lineNumber":109,"sourceCode":"             */\n            Deque<String[]> stack = new ArrayDeque<>();\n            stack.push(new String[] { traversalStartNode });\n            // This is used so that we know that we're unwinding the stack and know not to get the current node's neighbors again.\n            boolean unwinding = false;\n            while (stack.isEmpty() == false) {\n                String[] currentLevel = stack.peek();\n                int firstNonNullIndex = findFirstNonNull(currentLevel);\n                String node = currentLevel[firstNonNullIndex];\n                boolean endOfThisPath = false;\n                if (unwinding) {\n                    // We have completed all of this node's neighbors and have popped back to the node\n                    endOfThisPath = true;\n                } else if (traversalStartNode.equals(node) && stack.size() > 1) {\n                    Deque<String> reversedPath = new ArrayDeque<>();\n                    for (String[] level : stack) {\n                        reversedPath.push(level[findFirstNonNull(level)]);\n                    }\n                    throw new IllegalArgumentException(\"circular reference detected: \" + String.join(\"->\", reversedPath));\n                } else if (visitedFromThisStartNode.contains(node)) {\n                    /*\n                     * We are only looking for a cycle starting and ending at traversalStartNode right now. But this node has been\n                     * visited more than once in the path rooted at traversalStartNode. This could be because it is a cycle, or could be\n                     * because two nodes in the path both point to it. We add it to nodesVisitedMoreThanOnceInAPath so that we make sure\n                     * to check the path rooted at this node later.\n                     */\n                    nodesVisitedMoreThanOnceInAPath.add(node);\n                    endOfThisPath = true;\n                } else {\n                    visitedFromThisStartNode.add(node);\n                    String[] neighbors = getPatternNamesForPattern(bank, node);\n                    if (neighbors.length == 0) {\n                        endOfThisPath = true;\n                    } else {\n                        stack.push(neighbors);\n                    }\n                }","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/libs/grok/src/main/java/org/elasticsearch/grok/PatternBank.java#L91-L127","documentation":"Thrown by PatternBank's constructor (and extendWith) when a named grok pattern transitively references itself. The constructor runs forbidCircularReferences, which depth-first walks the directed graph of %{NAME} references; when the walk returns to the start node with a non-trivial stack, the path is reported as a cycle. The message lists the offending reference chain (start->...->start) so the conflicting pattern definitions are visible.","triggerScenarios":"Constructing `new PatternBank(map)` or calling `patternBank.extendWith(extra)` where two or more entries form a cycle, e.g. {\"A\":\"%{B}\", \"B\":\"%{A}\"} or a self-reference {\"A\":\"%{A}\"}. The check runs once at construction time, so the exception surfaces at the new PatternBank(...) call, not at match time.","commonSituations":"Loading a user-supplied grok pattern catalog (Logstash-style patterns.conf, ingest grok processor patterns) where one definition was renamed but a stale reference remained; copy-pasting a pattern that delegates to another which was later edited to delegate back; merging two pattern sets that each redefine a name pointing at the other.","solutions":["Read the -> chain in the message: it names the exact patterns forming the loop (e.g. A->B->A). Break the cycle by removing or rewriting one of those entries.","Audit every %{NAME} token in each named pattern in the chain and confirm each NAME resolves to a definition that does not lead back to the original.","Build the bank incrementally with extendWith to bisect which added pattern introduced the cycle.","If the cycle is intentional (aliasing), expand the alias inline so the graph stays acyclic, since PatternBank forbids cycles outright."],"exampleFix":"// before\nMap<String,String> patterns = new LinkedHashMap<>();\npatterns.put(\"IP\", \"%{HOSTNAME}\");\npatterns.put(\"HOSTNAME\", \"%{IP}\"); // cycle\nPatternBank bank = new PatternBank(patterns); // throws\n\n// after\npatterns.put(\"IP\", \"\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\");\npatterns.put(\"HOSTNAME\", \"\\\\b(?:[a-zA-Z0-9]+\\\\.)+[a-zA-Z]+\\\\b\");\nPatternBank bank = new PatternBank(patterns); // ok","handlingStrategy":"validation","validationCode":"// Validate a pattern map for cycles before constructing the bank.\nstatic void assertAcyclic(java.util.Map<String,String> patterns) {\n    java.util.Set<String> visiting = new java.util.HashSet<>();\n    java.util.Set<String> done = new java.util.HashSet<>();\n    for (String name : patterns.keySet()) dfs(name, patterns, visiting, done);\n}\nprivate static void dfs(String name, java.util.Map<String,String> patterns,\n                        java.util.Set<String> visiting, java.util.Set<String> done) {\n    if (done.contains(name) || !patterns.containsKey(name)) return;\n    if (!visiting.add(name)) throw new IllegalArgumentException(\"cycle at \" + name);\n    String body = patterns.get(name);\n    java.util.regex.Matcher m = java.util.regex.Pattern.compile(\"%\\\\{(\\\\w+)\").matcher(body);\n    while (m.find()) dfs(m.group(1), patterns, visiting, done);\n    visiting.remove(name);\n    done.add(name);\n}","typeGuard":"// Narrow to a bank whose construction succeeded (no cycle possible post-construction)\nstatic boolean isAcyclicMap(java.util.Map<String,String> patterns) {\n    try { new org.elasticsearch.grok.PatternBank(patterns); return true; }\n    catch (IllegalArgumentException e) { return false; }\n}","tryCatchPattern":"try {\n    PatternBank bank = new PatternBank(userPatterns);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"circular reference detected\")) {\n        // e.getMessage() contains the offending A->B->A chain\n        reportConfigError(\"grok patterns contain a cycle: \" + e.getMessage());\n    } else throw e;\n}","preventionTips":["Treat the pattern catalog as code: lint it in CI by attempting new PatternBank(map) before deploy.","Use extendWith to layer patterns so the cycle source is isolated to the most recently added layer.","Keep pattern names in a single registry and grep for %{NAME} before deleting a definition."],"tags":["grok","pattern","configuration","circular-reference"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T06:17:24.410Z"}