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

  1. Inspect the two placeholder names in the message — they form a cycle; break it by defining at least one with a literal value.
  2. Rename one of the mutually-referencing properties so the chain terminates.
  3. Ensure a placeholder never expands to a string containing its own placeholder name.
  4. When merging parameter sources, verify no key's value references itself or its merger counterpart.
  5. 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

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


AI-assisted analysis of apache/dolphinscheduler@02eac45a1b (2026-09-06). Data as JSON: /api/errors/168ed59249a85f67. Report an issue: GitHub.