elastic/elasticsearch · error · IllegalStateException

jar hell! duplicate jar on classpath

Error message

jar hell!
duplicate jar on classpath: {path}

What it means

Thrown as an IllegalStateException by JarHell.checkJarHell(Set<URL>, Consumer) during the deep duplicate scan when the same jar Path (after URL-to-Path conversion) is encountered twice across the merged classpath-and-modules URL set. Unlike error 478 (which catches text-level duplicates during string parsing), this catches path-level duplicates that survive URL resolution — typically when a jar enters the set through two different sources (e.g. once via the classpath and once via a module URL).

Solutions

  1. Identify which two URL sources contribute the duplicate jar (enable JarHell debug output via the Consumer).
  2. Remove the jar from one of the two locations — either the classpath entry or the module path entry.
  3. If the duplicate comes from a plugin, ensure the plugin does not bundle a jar already present in ES_HOME/lib.
  4. Rebuild the distribution/module layer so each jar appears in exactly one source set.
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the merged URL set (modules + classpath) has no duplicate jars
boolean noDuplicateJars(Collection<URL> urls) throws IOException {
    Set<Path> seen = new HashSet<>();
    for (URL u : urls) {
        if (u.toString().endsWith(".jar") && !seen.add(Path.of(u.toURI()))) return false;
    }
    return true;
}

Type guard

static boolean mergedUrlsHaveNoDuplicateJars(Collection<URL> urls) {
    try {
        Set<Path> seen = new HashSet<>();
        for (URL u : urls) {
            if (u.toString().endsWith(".jar") && !seen.add(Path.of(u.toURI()))) return false;
        }
        return true;
    } catch (Exception e) { return false; }
}

Try / catch

// IllegalStateException is fatal at startup; fix the classpath/module layout
// rather than catching at runtime. Identify the duplicate via JarHell's debug output.

Prevention

When it happens

Trigger: Calling JarHell.checkJarHell(urls, output) where the URL set (from parseModulesAndClassPath) contains the same jar path twice — e.g. a jar is both on the classpath and registered as a non-JDK boot module location. The seenJars.add fails at line 218.

Common situations: A custom module layer that re-exports a jar already on the classpath. Plugin modules that duplicate a base lib. A distribution packaging bug placing the same jar in both lib/ and a module path. Startup wiring that merges classpath and module URLs without dedup.

Related errors


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

Appendix: source

Thrown at libs/core/src/main/java/org/elasticsearch/jdk/JarHell.java:219

    @SuppressForbidden(reason = "needs JarFile for speed, just reading entries")
    public static void checkJarHell(Set<URL> urls, Consumer<String> output) throws IOException {
        // we don't try to be sneaky and use deprecated/internal/not portable stuff
        // like sun.boot.class.path, and with jigsaw we don't yet have a way to get
        // a "list" at all. So just exclude any elements underneath the java home
        String javaHome = System.getProperty("java.home");
        output.accept("java.home: " + javaHome);
        final Map<String, Path> clazzes = new HashMap<>(32768);
        Set<Path> seenJars = new HashSet<>();
        for (final URL url : urls) {
            final Path path = toPath(url);
            // exclude system resources
            if (path.startsWith(javaHome)) {
                output.accept("excluding system resource: " + path);
                continue;
            }
            if (path.toString().endsWith(".jar")) {
                if (seenJars.add(path) == false) {
                    throw new IllegalStateException("jar hell!" + System.lineSeparator() + "duplicate jar on classpath: " + path);
                }
                output.accept("examining jar: " + path);
                try (JarFile file = new JarFile(path.toString())) {
                    Manifest manifest = file.getManifest();
                    if (manifest != null) {
                        checkManifest(manifest, path);
                    }
                    // inspect entries
                    Enumeration<JarEntry> elements = file.entries();
                    while (elements.hasMoreElements()) {
                        String entry = elements.nextElement().getName();
                        if (entry.endsWith(".class")) {
                            // for jar format, the separator is defined as /
                            entry = entry.replace('/', '.').substring(0, entry.length() - 6);
                            checkClass(clazzes, entry, path);
                        }
                    }
                }

View on GitHub (pinned to db6a809a66)