elastic/elasticsearch · error · TestClustersException
Invalid system property `{}`. Use `requiresFeature` instead.
Error message
Invalid system property `{}`. Use `requiresFeature` instead. What it means
Thrown by getESEnvironment() while building ES_JAVA_OPTS when a user-supplied system property key contains the substring 'feature_flag'. The build reserves feature-flag activation for the requiresFeature API (which versions flags by from/until), so passing a raw -D...feature_flag... system property is rejected to prevent flag drift across versions and to keep flags version-gated.
Source
Thrown at build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java:831
}
// Older distributions ship with openjdk versions that are not compatible with newer kernels of ubuntu 24.04 and later
// Therefore we pass explicitly the runtime java to use the adoptium jdk that is maintained longer and compatible
// with newer kernels.
// 8.10.4 is the last version shipped with jdk < 21. We configure these cluster to run with jdk 17 adoptium as 17 was
// the last LTS release before 21
else if (jdkIsIncompatibleWithOS(getVersion())) {
defaultEnv.put(
"ES_JAVA_HOME",
jdk17FallbackLauncher.map(j -> j.getMetadata().getInstallationPath().getAsFile().getAbsolutePath()).get()
);
}
defaultEnv.put("ES_PATH_CONF", configFile.getParent().toString());
String systemPropertiesString = "";
if (systemProperties.isEmpty() == false) {
systemPropertiesString = " " + systemProperties.entrySet().stream().peek(entry -> {
if (entry.getKey().contains("feature_flag")) {
throw new TestClustersException("Invalid system property `" + entry.getKey() + "`. Use `requiresFeature` instead.");
}
})
.map(entry -> "-D" + entry.getKey() + "=" + entry.getValue())
// ES_PATH_CONF is also set as an environment variable and for a reference to ${ES_PATH_CONF}
// to work ES_JAVA_OPTS, we need to make sure that ES_PATH_CONF before ES_JAVA_OPTS. Instead,
// we replace the reference with the actual value in other environment variables
.map(p -> p.replace("${ES_PATH_CONF}", configFile.getParent().toString()))
.collect(Collectors.joining(" "));
}
if (systemProperties.containsKey("io.netty.leakDetection.level") == false) {
systemPropertiesString = systemPropertiesString + " -Dio.netty.leakDetection.level=paranoid";
}
String featureFlagsString = "";
if (featureFlags.isEmpty() == false && isReleasedVersion.apply(getVersion())) {
featureFlagsString = featureFlags.stream()
.filter(f -> getVersion().onOrAfter(f.getFrom()) && (f.getUntil() == null || getVersion().before(f.getUntil())))
.map(f -> "-D" + f.getFeature() + "=true")View on GitHub (pinned to db6a809a66)
Solutions
- Replace the system property with node.requiresFeature('es.feature_flag.foo', Version.fromString(...)).
- Use requiresFeature(feature, from) or requiresFeature(feature, from, until) to version-gate the flag.
- If the property genuinely is not a feature flag, rename the key so it does not contain 'feature_flag'.
- Audit existing systemProperty calls for the 'feature_flag' substring and migrate them.
Example fix
// before: forbidden raw system property
systemProperty('es.feature_flag.foo', 'true')
// after: version-gated feature flag
requiresFeature('es.feature_flag.foo', Version.fromString('8.15.0')) Defensive patterns
Strategy: validation
Validate before calling
static String assertNotFeatureFlagSystemProperty(String key) {
if (key.contains("feature_flag")) {
throw new IllegalArgumentException("Use requiresFeature for feature-flag key: " + key);
}
return key;
}
// Use: node.systemProperty(assertNotFeatureFlagSystemProperty(key), value); Type guard
static boolean isFeatureFlagKey(String key) {
return key != null && key.contains("feature_flag");
} Prevention
- Always route feature flags through requiresFeature(feature, from[, until]).
- Audit systemProperty calls for the 'feature_flag' substring when migrating tests.
- Rename non-flag keys that accidentally contain 'feature_flag'.
When it happens
Trigger: Calling node.systemProperty('feature.flag.xyz', 'true') or any key containing 'feature_flag' (note: the check is case-sensitive on the literal 'feature_flag', and looks for that substring, not 'feature.'). The throw fires lazily when getESEnvironment() runs during start() or bin-script exec.
Common situations: Copy-pasting a -Des.feature_flag.foo=true from production docs into a test cluster. Migrating an old test that pre-dates requiresFeature. Misunderstanding that feature flags must be version-scoped through requiresFeature.
Related errors
- Can't create extra config file from {} for {} as it does not
- extra jar file {} doesn't appear to be a JAR
- Not a valid module {} for {}
- extra config file destination can't be relative, was {} for
- Unknown keys in user definition {} for {}
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/a879e79082e2b7bc.
Report an issue: GitHub.