{"record":{"id":"168ed59249a85f67","repo":"apache/dolphinscheduler","slug":"circular-placeholder-reference-in-property-defi","errorCode":null,"errorMessage":"Circular placeholder reference '' in property definitions","messagePattern":"Circular placeholder reference '' in property definitions","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/parser/PropertyPlaceholderHelper.java","lineNumber":138,"sourceCode":"    public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {\n        notNull(value, \"'value' must not be null\");\n        return parseStringValue(value, placeholderResolver, new HashSet<>());\n    }\n\n    protected String parseStringValue(\n                                      String value, PlaceholderResolver placeholderResolver,\n                                      Set<String> visitedPlaceholders) {\n\n        StringBuilder result = new StringBuilder(value);\n\n        int startIndex = value.indexOf(this.placeholderPrefix);\n        while (startIndex != -1) {\n            int endIndex = findPlaceholderEndIndex(result, startIndex);\n            if (endIndex != -1) {\n                String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);\n                String originalPlaceholder = placeholder;\n                if (!visitedPlaceholders.add(originalPlaceholder)) {\n                    throw new IllegalArgumentException(\n                            \"Circular placeholder reference '\" + originalPlaceholder + \"' in property definitions\");\n                }\n                // Recursive invocation, parsing placeholders contained in the placeholder key.\n                placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);\n                // Now obtain the value for the fully resolved key...\n                String propVal = placeholderResolver.resolvePlaceholder(placeholder);\n                if (propVal == null && this.valueSeparator != null) {\n                    int separatorIndex = placeholder.indexOf(this.valueSeparator);\n                    if (separatorIndex != -1) {\n                        String actualPlaceholder = placeholder.substring(0, separatorIndex);\n                        String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());\n                        propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);\n                        if (propVal == null) {\n                            propVal = defaultValue;\n                        }\n                    }\n                }\n                if (propVal != null) {","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/apache/dolphinscheduler/blob/02eac45a1b6676e639fcbfb4be2243de5771b05d/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/parser/PropertyPlaceholderHelper.java#L120-L156","documentation":"PropertyPlaceholderHelper detects infinite recursion while resolving ${...} placeholders: when a placeholder's name (possibly after nested resolution) reappears in visitedPlaceholders, resolution would never terminate, so it throws IllegalArgumentException('Circular placeholder reference ... in property definitions'). This mirrors Spring's PropertyPlaceholderHelper behavior.","triggerScenarios":"Configuring properties like a=${b}, b=${a}, or self-reference x=${x}; a placeholder resolving to text containing its own name (e.g. name=${name}_suffix); deep nested placeholders whose inner key resolves back to an outer key already being expanded.","commonSituations":"Merged config from multiple sources (global params + local params + env) where two params point at each other; copy-pasting a property into its own value; after a rename, an old key now referencing the new key which references the old.","solutions":["Inspect the two placeholder names in the message — they form a cycle; break it by defining at least one with a literal value.","Rename one of the mutually-referencing properties so the chain terminates.","Ensure a placeholder never expands to a string containing its own placeholder name.","When merging parameter sources, verify no key's value references itself or its merger counterpart.","If unresolvable references are acceptable, use ignoreUnresolvablePlaceholders=true — though true cycles still throw."],"exampleFix":"// before\nprops: \"path=${base}/data\", \"base=${path}/root\"   // circular\n// after\nprops: \"base=/opt/app\", \"path=${base}/data\"       // acyclic","handlingStrategy":"validation","validationCode":"Set<String> seen = new HashSet<>();\nfor (String k : props.stringPropertyNames()) {\n    String cur = k; int steps = 0;\n    while (cur != null && seen.add(cur) && steps++ < 100) {\n        String v = props.getProperty(cur);\n        Matcher m = Pattern.compile(\"\\\\$\\\\{([^}]+)}\").matcher(v == null ? \"\" : v);\n        cur = m.find() ? m.group(1) : null; // follow one edge; cycle => depth exceed\n    }\n}","typeGuard":null,"tryCatchPattern":"try { resolved = helper.replacePlaceholders(value, resolver); } catch (IllegalArgumentException e) { throw new ConfigException(\"circular parameter reference in workflow params\", e); }","preventionTips":["Keep a strict DAG of parameter references; forbid self/cyclic references in custom params","Never let a property's value contain its own placeholder name","Test merged parameter sets for cycles in CI","Use distinct key namespaces for global vs local params to avoid accidental cross-references"],"tags":["placeholders","configuration","recursion"],"backgroundTag":"circular-placeholder-reference","analyzedSha":"02eac45a1b6676e639fcbfb4be2243de5771b05d","analyzedAt":"2026-09-06T17:43:00.555Z","contentChangedAt":"2026-09-06T17:43:00.555Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}