pinpoint-apm/pinpoint · error · IllegalArgumentException

Circular placeholder reference '<placeholder>' in property d

Error message

Circular placeholder reference '<placeholder>' in property definitions

What it means

PropertyPlaceholderHelper.replacePlaceholders()/parseStringValue() recursively resolves ${...} placeholders in a property string. To detect infinite recursion it tracks visited placeholders; if the same placeholder name is encountered again before resolution completes (a placeholder referencing itself directly or through a chain), it throws IllegalArgumentException naming the circular reference. In ValueAnnotationProcessor the helper is created with ignoreUnresolvablePlaceholders=false, so errors propagate.

Source

Thrown at commons-config/src/main/java/com/navercorp/pinpoint/common/config/util/spring/PropertyPlaceholderHelper.java:137

    public String replacePlaceholders(String value, Function<String, String> placeholderResolver) {
        Objects.requireNonNull(value, "value");

        return parseStringValue(value, placeholderResolver, new HashSet<>());
    }

    protected String parseStringValue(
            String strVal, Function<String, String> placeholderResolver, Set<String> visitedPlaceholders) {

        StringBuilder buf = new StringBuilder(strVal);

        int startIndex = strVal.indexOf(this.placeholderPrefix);
        while (startIndex != -1) {
            int endIndex = findPlaceholderEndIndex(buf, startIndex);
            if (endIndex != -1) {
                String placeholder = buf.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.apply(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.apply(actualPlaceholder);
                        if (propVal == null) {
                            propVal = defaultValue;
                        }
                    }
                }
                if (propVal != null) {

View on GitHub (pinned to 744c3d3075)

Solutions

  1. Break the cycle by making at least one property hold a literal value instead of a placeholder
  2. Rename one of the chained keys and update its references
  3. Inline the final value directly into the @Value annotation or the property file entry

Example fix

// before (properties)
a=${b}
b=${a}
// after (properties)
a=${b}
b=literalValue
Defensive patterns

Strategy: validation

Validate before calling

// Detect cycles in a properties map before processing
Map<String,String> props = ...;
Set<String> visiting = new HashSet<>();
for (String key : props.keySet()) {
    Deque<String> stack = new ArrayDeque<>(List.of(key));
    while (!stack.isEmpty()) {
        String k = stack.pop();
        if (!visiting.add(k)) throw new IllegalStateException("Circular placeholder: " + k);
        Matcher m = Pattern.compile("\\$\\{([^}]+)\\}").matcher(String.valueOf(props.getOrDefault(k, "")));
        while (m.find()) stack.push(m.group(1));
    }
}

Try / catch

try {
    String resolved = helper.replacePlaceholders(raw, resolver);
} catch (IllegalArgumentException e) {
    log.error("Placeholder cycle: {}", e.getMessage());
}

Prevention

When it happens

Trigger: A property key's value contains a placeholder that resolves (possibly through several hops, A -> B -> A) back to itself, e.g. properties file has a=${b}, b=${a}, and @Value("${a}") is processed.

Common situations: Copy-pasting property chains in config files creating accidental cycles; environment-variable fallbacks that reference each other; migrating property sets between files where a key was renamed but the old reference remains, forming a loop.

Related errors


AI-assisted analysis of pinpoint-apm/pinpoint@744c3d3075 (2026-09-07). Data as JSON: /api/errors/2ee6c5e9d2a6b0b2. Report an issue: GitHub.