{"record":{"id":"a879e79082e2b7bc","repo":"elastic/elasticsearch","slug":"invalid-system-property-use-requiresfeature","errorCode":null,"errorMessage":"Invalid system property `{}`. Use `requiresFeature` instead.","messagePattern":"Invalid system property `(.+?)`\\. Use `requiresFeature` instead\\.","errorType":"validation","errorClass":"TestClustersException","httpStatus":null,"severity":"error","filePath":"build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java","lineNumber":831,"sourceCode":"        }\n        // Older distributions ship with openjdk versions that are not compatible with newer kernels of ubuntu 24.04 and later\n        // Therefore we pass explicitly the runtime java to use the adoptium jdk that is maintained longer and compatible\n        // with newer kernels.\n        // 8.10.4 is the last version shipped with jdk < 21. We configure these cluster to run with jdk 17 adoptium as 17 was\n        // the last LTS release before 21\n        else if (jdkIsIncompatibleWithOS(getVersion())) {\n            defaultEnv.put(\n                \"ES_JAVA_HOME\",\n                jdk17FallbackLauncher.map(j -> j.getMetadata().getInstallationPath().getAsFile().getAbsolutePath()).get()\n            );\n        }\n        defaultEnv.put(\"ES_PATH_CONF\", configFile.getParent().toString());\n\n        String systemPropertiesString = \"\";\n        if (systemProperties.isEmpty() == false) {\n            systemPropertiesString = \" \" + systemProperties.entrySet().stream().peek(entry -> {\n                if (entry.getKey().contains(\"feature_flag\")) {\n                    throw new TestClustersException(\"Invalid system property `\" + entry.getKey() + \"`. Use `requiresFeature` instead.\");\n                }\n            })\n                .map(entry -> \"-D\" + entry.getKey() + \"=\" + entry.getValue())\n                // ES_PATH_CONF is also set as an environment variable and for a reference to ${ES_PATH_CONF}\n                // to work ES_JAVA_OPTS, we need to make sure that ES_PATH_CONF before ES_JAVA_OPTS. Instead,\n                // we replace the reference with the actual value in other environment variables\n                .map(p -> p.replace(\"${ES_PATH_CONF}\", configFile.getParent().toString()))\n                .collect(Collectors.joining(\" \"));\n        }\n        if (systemProperties.containsKey(\"io.netty.leakDetection.level\") == false) {\n            systemPropertiesString = systemPropertiesString + \" -Dio.netty.leakDetection.level=paranoid\";\n        }\n\n        String featureFlagsString = \"\";\n        if (featureFlags.isEmpty() == false && isReleasedVersion.apply(getVersion())) {\n            featureFlagsString = featureFlags.stream()\n                .filter(f -> getVersion().onOrAfter(f.getFrom()) && (f.getUntil() == null || getVersion().before(f.getUntil())))\n                .map(f -> \"-D\" + f.getFeature() + \"=true\")","sourceCodeStart":813,"sourceCodeEnd":849,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java#L813-L849","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: forbidden raw system property\nsystemProperty('es.feature_flag.foo', 'true')\n// after: version-gated feature flag\nrequiresFeature('es.feature_flag.foo', Version.fromString('8.15.0'))","handlingStrategy":"validation","validationCode":"static String assertNotFeatureFlagSystemProperty(String key) {\n    if (key.contains(\"feature_flag\")) {\n        throw new IllegalArgumentException(\"Use requiresFeature for feature-flag key: \" + key);\n    }\n    return key;\n}\n// Use: node.systemProperty(assertNotFeatureFlagSystemProperty(key), value);","typeGuard":"static boolean isFeatureFlagKey(String key) {\n    return key != null && key.contains(\"feature_flag\");\n}","tryCatchPattern":null,"preventionTips":["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'."],"tags":["testclusters","feature-flags","validation","config"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T12:17:08.281Z"}