apache/dolphinscheduler · error · IllegalArgumentException
Circular placeholder reference '' in property definitions
Error message
Circular placeholder reference '' in property definitions
What it means
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.
Source
Thrown at dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/parser/PropertyPlaceholderHelper.java:138
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, new HashSet<>());
}
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver,
Set<String> visitedPlaceholders) {
StringBuilder result = new StringBuilder(value);
int startIndex = value.indexOf(this.placeholderPrefix);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// Recursive invocation, parsing placeholders contained in the placeholder key.
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0, separatorIndex);
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
if (propVal != null) {View on GitHub (pinned to 02eac45a1b)
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.
Example fix
// before
props: "path=${base}/data", "base=${path}/root" // circular
// after
props: "base=/opt/app", "path=${base}/data" // acyclic Defensive patterns
Strategy: validation
Validate before calling
Set<String> seen = new HashSet<>();
for (String k : props.stringPropertyNames()) {
String cur = k; int steps = 0;
while (cur != null && seen.add(cur) && steps++ < 100) {
String v = props.getProperty(cur);
Matcher m = Pattern.compile("\\$\\{([^}]+)}").matcher(v == null ? "" : v);
cur = m.find() ? m.group(1) : null; // follow one edge; cycle => depth exceed
}
} Try / catch
try { resolved = helper.replacePlaceholders(value, resolver); } catch (IllegalArgumentException e) { throw new ConfigException("circular parameter reference in workflow params", e); } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Could not resolve placeholder '
- receivers must not be null
- url can not be null
- headerParams is not a valid json
- bodyParams is not a valid json
AI-assisted analysis of apache/dolphinscheduler@02eac45a1b (2026-09-06).
Data as JSON: /api/errors/168ed59249a85f67.
Report an issue: GitHub.