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
- Check the resolvedPath in the message and ensure it points to the actual driver .jar file
- Fix the dynamic driver configuration/path so the resolver finds the JAR (not its containing directory)
- Verify JdbcDriverResolver's fallback chain and the driverId spelling
- 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
- Point dynamic driver config at actual .jar files, not directories or zips
- Pre-validate resolved paths during deployment scripts
- Keep driver repositories in a documented, flat layout
- Extract zipped drivers and reference the inner JAR
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
- Dynamic driver: failed to load
- Dynamic driver ' ' does not accept URL: — check the JDBC…
- Dynamic driver ' ' failed to connect to URL
- Dynamic driver ' ' returned null for URL: — check that the…
- Dynamic driver ' ' threw exception checking URL
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)