quarkusio/quarkus · error · UnsupportedOperationException

OSGi is not supported by quarkus-liquibase

Error message

OSGi is not supported by quarkus-liquibase

What it means

quarkus-liquibase substitutes Liquibase's liquibase.util.OsgiUtil.loadClass with a GraalVM @Substitute that always throws UnsupportedOperationException, because OSGi classloading is meaningless in a native Quarkus image. It only fires when Liquibase code running inside a native executable attempts to resolve a class through its OSGi path instead of the plain Class.forName path. This is intentionally unsupported behavior, not a bug.

Source

Thrown at extensions/liquibase/liquibase-common/src/main/java/io/quarkus/liquibase/common/runtime/graal/SubstituteOsgiUtil.java:11

package io.quarkus.liquibase.common.runtime.graal;

import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

@TargetClass(liquibase.util.OsgiUtil.class)
final class SubstituteOsgiUtil {

    @Substitute
    public static <T> Class<T> loadClass(String className) throws ClassNotFoundException {
        throw new UnsupportedOperationException("OSGi is not supported by quarkus-liquibase");
    }
}

View on GitHub (pinned to e1c734241f)

Solutions

  1. Verify the load happens only in native mode; test the same flow in JVM mode to isolate the OSGi substitute path
  2. Remove or restructure the custom Liquibase extension/class so it is discovered via standard ServiceLoader/Class.forName instead of OSGi (register it via META-INF/services or liquibase.locator ServiceLocator)
  3. Pin/upgrade liquibase version bundled by quarkus-liquibase so the OSGi code path is not taken
  4. If the class is truly needed, add your own GraalVM substitution/feature for liquibase.util.OsgiUtil or the calling code in your app
Defensive patterns

Strategy: try-catch

Validate before calling

// Native apps: ensure custom Liquibase classes are loaded via ServiceLoader before Liquibase runs
if (Class.forName("my.pkg.MyCustomChange", true, Thread.currentThread().getContextClassLoader()) == null) {
    throw new IllegalStateException("Custom Liquibase class not on classpath; OsgiUtil path will fail in native");
}

Type guard

boolean isNativeImage = System.getProperty("org.graalvm.nativeimage.iscompilation") != null
        || Class.forName("com.oracle.svm.core.annotate.Substitute", false, ClassLoader.getSystemClassLoader()) != null;

Try / catch

try {
    liquibase.update(context);
} catch (UnsupportedOperationException e) {
    if (e.getMessage() != null && e.getMessage().contains("OSGi is not supported")) {
        throw new IllegalStateException("Liquibase tried OSGi classloading in native mode; register classes via ServiceLoader", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Running a Quarkus application in native-image mode where Liquibase calls OsgiUtil.loadClass(className) — typically during changelog parsing/custom class resolution (e.g. custom ChangeLogParser, ChangeGenerator, or extension classes registered only via OSGi service discovery) in a native build.

Common situations: Users shipping Liquibase extensions or custom classes (CustomChangeWrapper, custom task/changes) that Liquibase tries to load via the OSGi branch in native mode; unusual Liquibase versions where class lookup prefers the OSGi path; native-image builds where no substitute for the actual loading path exists.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/e23d55d167a85315. Report an issue: GitHub.