elastic/elasticsearch · error · InvalidUserDataException

classname is a forbidden for stable esplugin

Error message

classname is a forbidden for stable esplugin

What it means

The mirror of error 41: thrown when a plugin declared as stable (isStable=true) ALSO supplies a classname. Stable plugins intentionally have no Plugin entry-point class, so supplying classname contradicts the stable contract and the task refuses to emit it.

Source

Thrown at build-tools/src/main/java/org/elasticsearch/gradle/plugin/GeneratePluginPropertiesTask.java:116

    @OutputFile
    public abstract RegularFileProperty getOutputFile();

    @Input
    public abstract Property<Boolean> getIsStable();

    @Input
    @Optional
    public abstract Property<String> getDeploymentTarget();

    @TaskAction
    public void generatePropertiesFile() throws IOException {
        String classname = getClassname().getOrElse("");
        boolean stablePlugin = getIsStable().getOrElse(false);
        if (stablePlugin == false && classname.isEmpty()) {
            throw new InvalidUserDataException("classname is a required setting for esplugin");
        }
        if (stablePlugin && classname.isEmpty() == false) {
            throw new InvalidUserDataException("classname is a forbidden for stable esplugin");
        }

        Map<String, Object> props = new HashMap<>();
        props.put("name", getPluginName().get());
        props.put("description", getPluginDescription().get());
        props.put("version", getPluginVersion().get());
        props.put("elasticsearchVersion", getElasticsearchVersion().get());
        props.put("javaVersion", getJavaVersion().get());
        props.put("classname", classname);
        props.put("extendedPlugins", String.join(",", getExtendedPlugins().get()));
        props.put("hasNativeController", getHasNativeController().get());
        props.put("nativeControllerEnabledSettings", String.join(",", getNativeControllerEnabledSettings().get()));
        props.put("requiresKeystore", getRequiresKeystore().get());
        props.put("licensed", getIsLicensed().get());
        props.put("modulename", findModuleName());

        String deploymentTarget = getDeploymentTarget().getOrElse("ALL");
        if (DEPLOYMENT_TARGETS.contains(deploymentTarget) == false) {

View on GitHub (pinned to db6a809a66)

Solutions

  1. Remove the `classname '...'` line from the esplugin block for stable plugins.
  2. If you actually need a Plugin entry class, set `isStable = false` (the default) so classname becomes required instead of forbidden.
  3. Re-run the generatePluginPropertiesTask to confirm it emits a descriptor with no java policy/plugin entry class.

Example fix

// before
esplugin {
  name 'my-stable-plugin'
  isStable = true
  classname 'com.acme.MyPlugin'  // forbidden when stable
}

// after
esplugin {
  name 'my-stable-plugin'
  isStable = true
  // no classname
}
Defensive patterns

Strategy: validation

Validate before calling

project.tasks.withType(GeneratePluginPropertiesTask.class).configureEach {
    if (it.isStable.get() && !it.classname.getOrElse('').trim().isEmpty()) {
        throw new GradleException("esplugin '${it.pluginName.get()}' is stable — remove classname")
    }
}

Prevention

When it happens

Trigger: esplugin { isStable = true; classname = '...' } — both set at once. The second guard in generatePropertiesFile fires.

Common situations: Converting a regular plugin to a stable plugin and forgetting to remove classname, or copy-pasting an esplugin block and toggling isStable without clearing classname.

Understand the failure class

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/09034415cde24e50. Report an issue: GitHub.