{"record":{"id":"156e13c544af5c50","repo":"apache/maven","slug":"recursive-variable-reference-variable","errorCode":null,"errorMessage":"recursive variable reference: ${variable}","messagePattern":"recursive variable reference: (.+?)","errorType":"exception","errorClass":"InterpolatorException","httpStatus":null,"severity":"error","filePath":"impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultInterpolator.java","lineNumber":372,"sourceCode":"            } else {\n                substValue = MARKER + \"{\" + variable + \"}\";\n            }\n        }\n\n        return substValue;\n    }\n\n    private static String resolveVariable(\n            String variable,\n            Set<String> cycleMap,\n            Map<String, String> configProps,\n            UnaryOperator<String> callback,\n            BinaryOperator<String> postprocessor,\n            boolean defaultsToEmptyString) {\n\n        // Verify that this is not a recursive variable reference\n        if (!cycleMap.add(variable)) {\n            throw new InterpolatorException(\"recursive variable reference: \" + variable);\n        }\n\n        String substValue = null;\n        // Try configuration properties first\n        if (configProps != null) {\n            substValue = configProps.get(variable);\n        }\n        if (substValue == null && !variable.isEmpty() && callback != null) {\n            String s1 = callback.apply(variable);\n            String s2 =\n                    doSubstVars(s1, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);\n            substValue = postprocessor != null ? postprocessor.apply(variable, s2) : s2;\n        }\n\n        // Remove the variable from cycle map\n        cycleMap.remove(variable);\n        return substValue;\n    }","sourceCodeStart":354,"sourceCodeEnd":390,"githubUrl":"https://github.com/apache/maven/blob/e4093d4e120eac99d6bdce5ba67cace2f3085c97/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultInterpolator.java#L354-L390","documentation":"Thrown by Maven's DefaultInterpolator while expanding ${...} placeholders in a POM or settings. resolveVariable() tracks every variable currently being resolved in a cycleMap; if resolving a variable requires the same variable again (directly, like <a>${a}</a>, or through a chain like a=${b}, b=${a}), the duplicate cycleMap.add() fails and InterpolatorException reports the offending variable name. The check is what stops model interpolation from recursing forever.","triggerScenarios":"A property whose value contains its own placeholder, or two or more properties that reference each other in a ring, hit while building the effective model, interpolating profile activation conditions, or resolving ${...} expressions in dependencies/versions.","commonSituations":"Copy-pasted property blocks in pom.xml or settings.xml profiles; a profile that redefines a property to itself to 'keep the default'; CI injecting a -D override that loops back to the same name; property chains introduced during a Maven upgrade.","solutions":["Search pom.xml, parent poms, settings.xml profiles and -D overrides for the variable named in the message and break the self- or mutual reference","If the intent was a default value, put the literal default in the property and override it from the CLI (-Dname=value) instead of self-referencing","Remove or correct the redefinition in the active profile (check both the POM and ~/.m2/settings.xml)","Run with -X -e to see which model and which property triggered the interpolation chain"],"exampleFix":"<!-- before -->\n<properties>\n  <docker.tag>${docker.tag}</docker.tag>\n</properties>\n\n<!-- after -->\n<properties>\n  <docker.tag>latest</docker.tag>\n</properties>\n<!-- override at will with: mvn deploy -Ddocker.tag=1.2.3 -->","handlingStrategy":"try-catch","validationCode":"// Pre-check a property map for cycles before interpolating\nstatic List<String> findPropertyCycle(Map<String, String> props) {\n    for (String start : props.keySet()) {\n        Set<String> seen = new LinkedHashSet<>();\n        String cur = start;\n        while (cur != null) {\n            if (!seen.add(cur)) {\n                return new ArrayList<>(seen); // cycle: last element repeats\n            }\n            String v = props.get(cur);\n            Matcher m = Pattern.compile(\"\\\\$\\\\{([^}]+)\\\\}\").matcher(v == null ? \"\" : v);\n            cur = m.find() ? m.group(1) : null;\n        }\n    }\n    return List.of();\n}","typeGuard":null,"tryCatchPattern":"try {\n    String out = interpolator.interpolate(input, ctx);\n} catch (InterpolatorException e) {\n    // e.getMessage() names the recursive variable; fall back to the raw value or abort\n    log.warn(\"Unresolvable expression, keeping raw value: {}\", e.getMessage());\n    return input;\n}","preventionTips":["Never define a property whose value references itself; use a literal default plus a -D override","Lint POMs and settings profiles for property references and reject any node that appears in its own dependency chain","When merging property maps (parent + profiles + CLI), assert the merge does not introduce a reference loop"],"tags":["maven","interpolation","properties","cycle"],"backgroundTag":"circular-property-reference","analyzedSha":"e4093d4e120eac99d6bdce5ba67cace2f3085c97","analyzedAt":"2026-08-21T22:58:24.034Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}