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.
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 '
- bodyParams is not a valid json
- bucketName: is not exists, you need to create them by…
- bucketName: is not exists, you need to create them by…
- bucketName: is not exists, you need to create them by…
AI-assisted analysis of apache/dolphinscheduler@02eac45a1b (2026-09-06).
Data as JSON: /api/errors/168ed59249a85f67.
Report an issue: GitHub.
Appendix: 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)