pentaho/pentaho-kettle · error · KettleDatabaseException

Dynamic driver path does not point to a JAR file

Error message

Dynamic driver path does not point to a JAR file: {resolvedPath}

What it means

loadDynamicDriver resolves a driverId to a file path via JdbcDriverResolver.resolve and then requires that path to end with '.jar'. If resolution returns a directory, a metadata URL, or a non-jar artifact, loading cannot proceed and this error is thrown. It guards the URLClassLoader which needs an actual JAR.

Solutions

  1. Check the resolvedPath in the message and ensure it points to the actual driver .jar file
  2. Fix the dynamic driver configuration/path so the resolver finds the JAR (not its containing directory)
  3. Verify JdbcDriverResolver's fallback chain and the driverId spelling
  4. If the driver ships as .zip, extract and point to the contained JAR

Example fix

// before
String path = "/opt/drivers/mysql-connector-j-8.4.0"; // directory
// after
String path = "/opt/drivers/mysql-connector-j-8.4.0/mysql-connector-j-8.4.0.jar";
Defensive patterns

Strategy: validation

Validate before calling

File f = new File(JdbcDriverResolver.resolve(driverId));
if (!f.isFile() || !f.getName().toLowerCase().endsWith(".jar")) throw new IllegalStateException("Not a JAR: " + f);

Type guard

boolean isJarPath(String p) { return p != null && p.toLowerCase().endsWith(".jar") && new File(p).isFile(); }

Try / catch

try { db.connect(); } catch (KettleDatabaseException e) { if (e.getMessage().startsWith("Dynamic driver path does not point to a JAR file")) { log.error("Fix driver path: " + e.getMessage()); } else throw e; }

Prevention

When it happens

Trigger: connectUsingClass -> loadDynamicDriver for a driverId whose JdbcDriverResolver.resolve returns a path not ending in .jar — e.g. resolver falls back to a folder, the configured driver path points to a directory, or the artifact is a .zip/.so.

Common situations: Misconfigured dynamic-driver directory pointing at an extracted (unpacked) driver instead of the JAR; resolver fallback chain returns a base path; user placed a zip download of a driver without renaming; driver repository layout changed.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of pentaho/pentaho-kettle@f3058517a1 (2026-09-13). Data as JSON: /api/errors/9f37d8f6477c8a74. Report an issue: GitHub.

Appendix: source

Thrown at core/src/main/java/org/pentaho/di/core/database/Database.java:896

   * <p><b>Cached path</b> ({@code KETTLE_DYNAMIC_DRIVER_CACHE_ENABLED=Y}):
   * Delegates to {@link DynamicDriverCache#getOrLoadDriver}, which reuses the same
   * {@link Driver} instance and classloader JVM-wide. No JAR reads after the first connect.
   * {@link #dynamicDriverClassLoader} is left {@code null} because the classloader lifetime
   * is managed by {@link DynamicDriverCache}, not by this {@link Database} instance.
   *
   * <p><b>No-cache path</b> (default — property absent or not {@code Y}):
   * Creates a fresh {@link ChildFirstURLClassLoader} per connection, loads the driver class,
   * and stores the loader in {@link #dynamicDriverClassLoader} so that
   * {@link #closeDynamicClassLoader()} closes it (and releases the JAR file handle) on
   * {@link #disconnect()}. Each connection is fully isolated with no shared JVM state.
   */
  private void loadDynamicDriver( String driverId, String effectiveClassName, List<String> listDriverExtraJars ) throws KettleDatabaseException {
    // Always resolve first — returns immediately if driverId exists on disk,
    // otherwise walks the fallback chain. The resolved path is stable for the connection lifetime.
    String resolvedPath = JdbcDriverResolver.resolve( driverId );

    if ( !resolvedPath.toLowerCase().endsWith( ".jar" ) ) {
      throw new KettleDatabaseException( "Dynamic driver path does not point to a JAR file: " + resolvedPath );
    }

    List<String> extraJarPaths = JdbcDriverResolver.resolveAll( listDriverExtraJars );
    boolean cacheEnabled = "Y".equalsIgnoreCase(
      EnvUtil.getSystemProperty( Const.KETTLE_DYNAMIC_DRIVER_CACHE_ENABLED ) );

    if ( cacheEnabled ) {
      // Cached path: Driver is shared JVM-wide; classloader is owned by DynamicDriverCache.
      dynamicDriver.set( DynamicDriverCache.getInstance().getOrLoadDriver( resolvedPath, effectiveClassName, extraJarPaths ) );
      dynamicDriverClassLoader.set( null );
    } else {
      // No-cache path: fresh classloader and Driver per connection; closed on disconnect().
      List<URL> urls = JdbcDriverResolver.buildUrlList( resolvedPath, extraJarPaths );
      ChildFirstURLClassLoader loader = null;
      try {
        loader = new ChildFirstURLClassLoader( urls.toArray( new URL[ 0 ] ), Database.class.getClassLoader() );
        Class<?> driverClass = loader.loadClass( effectiveClassName );
        Driver driver = (Driver) driverClass.getDeclaredConstructor().newInstance();

View on GitHub (pinned to f3058517a1)