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
- Break the cycle by making at least one property hold a literal value instead of a placeholder
- Rename one of the chained keys and update its references
- 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
- Never let property chains reference each other back to the original key
- Keep at most one level of placeholder indirection where possible
- Note ValueAnnotationProcessor.getValue() silently swallows IllegalArgumentException and returns null — validate that the expected value actually resolved
- Add a startup test that resolves every property key once
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
- Could not resolve placeholder '<placeholder>' in string valu
- %s load fail Caused by:%s
- %s load fail Caused by:%s
- Unknown AgentType:
- Failed to detect pinpoint profile. Please add -Dpinpoint.act
AI-assisted analysis of pinpoint-apm/pinpoint@744c3d3075 (2026-09-07).
Data as JSON: /api/errors/2ee6c5e9d2a6b0b2.
Report an issue: GitHub.