quarkusio/quarkus · error · java.lang.RuntimeException

@PermissionChecker method '%s' has return type '%s', but onl

Error message

@PermissionChecker method '%s' has return type '%s', but only supported return types are 'boolean' and 'Uni<Boolean>'. 

What it means

A @PermissionChecker method must return either a primitive boolean (synchronous check) or Uni<Boolean> (reactive check). Any other return type cannot be interpreted by the permission runtime, so the build fails with this message naming the actual type.

Source

Thrown at extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java:143

        private static Map<String, PermissionCheckerMetadata> getPermissionCheckers(IndexView index) {
            int permissionCheckerIndex = 0; // this ensures generated QuarkusPermission name is unique
            var permissionCheckers = new HashMap<String, PermissionCheckerMetadata>();
            for (var annotationInstance : index.getAnnotations(PERMISSION_CHECKER_NAME)) {
                var checkerMethod = annotationInstance.target().asMethod();
                if (Modifier.isPrivate(checkerMethod.flags())) {
                    // we generate QuarkusPermission in the same package as where the @PermissionChecker is detected
                    // so the checker method must be either public or package-private
                    throw new RuntimeException("Private method '" + toString(checkerMethod)
                            + "' cannot be annotated with the @PermissionChecker annotation");
                }
                if (Modifier.isStatic(checkerMethod.flags())) {
                    // checkers must be CDI bean member methods for now, so the checker method must not be static
                    throw new RuntimeException("Static method '" + toString(checkerMethod)
                            + "' cannot be annotated with the @PermissionChecker annotation");
                }
                boolean isReactive = isUniBoolean(checkerMethod);
                if (!isReactive && !isPrimitiveBoolean(checkerMethod)) {
                    throw new RuntimeException(("@PermissionChecker method '%s' has return type '%s', but only " +
                            "supported return types are 'boolean' and 'Uni<Boolean>'. ")
                            .formatted(toString(checkerMethod), checkerMethod.returnType().name()));
                }

                var permissionName = annotationInstance.value().asString();
                if (permissionName.isBlank()) {
                    throw new IllegalArgumentException(
                            "@PermissionChecker annotation placed on the '%s' attribute 'value' must not be blank"
                                    .formatted(toString(checkerMethod)));
                }
                boolean isBlocking = checkerMethod.hasDeclaredAnnotation(BLOCKING);
                if (isBlocking && isReactive) {
                    throw new IllegalArgumentException("""
                            @PermissionChecker annotation instance placed on the '%s' returns 'Uni<Boolean>' and is
                            annotated with the @Blocking annotation; if you need to block, please return 'boolean'
                            """.formatted(toString(checkerMethod)));
                }

View on GitHub (pinned to e1c734241f)

Solutions

  1. Change the return type to primitive boolean for synchronous checks
  2. Change the return type to Uni<Boolean> for reactive checks (SmallRye Mutiny)
  3. If using CompletionStage, convert it: Uni.createFrom().completionStage(...)
  4. If returning null/optional semantics, restructure to return boolean with explicit false

Example fix

// before
@PermissionChecker("can-edit")
Boolean canEdit(Document doc) { return doc != null && doc.ownerId != null; }
// after
@PermissionChecker("can-edit")
boolean canEdit(Document doc) { return doc != null && doc.ownerId != null; }
// reactive alternative
@PermissionChecker("can-edit")
Uni<Boolean> canEditAsync(Document doc) { return authService.can(doc.ownerId).map(o -> o != null); }
Defensive patterns

Strategy: validation

Validate before calling

for (Method m : SecurityChecks.class.getDeclaredMethods()) {
    if (m.isAnnotationPresent(PermissionChecker.class)) {
        Class<?> r = m.getReturnType();
        if (r != boolean.class && !io.smallrye.mutiny.Uni.class.equals(r)) {
            throw new IllegalStateException("@PermissionChecker " + m.getName()
                + " must return boolean or Uni<Boolean>, got: " + r);
        }
    }
}

Type guard

boolean hasValidCheckerReturnType(java.lang.reflect.Method m) {
    Class<?> r = m.getReturnType();
    return r == boolean.class || io.smallrye.mutiny.Uni.class.equals(r);
}

Try / catch

try {
    appBootstrap();
} catch (RuntimeException e) {
    if (e.getMessage() != null && e.getMessage().contains("only supported return types are 'boolean' and 'Uni<Boolean>'")) {
        throw new IllegalStateException("Change @PermissionChecker return type to boolean or Uni<Boolean>", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Annotating a method whose return type is e.g. Boolean (boxed), Optional<Boolean>, CompletionStage<Boolean>, Uni<Boolean> with generics mismatch (isUniBoolean checks the exact type/signature), int, or void with @PermissionChecker.

Common situations: Returning boxed Boolean from a helper that previously could return null; migrating a synchronous checker to async and accidentally using CompletionStage<Boolean> instead of Uni<Boolean>; Kotlin developers returning Boolean? .

Related errors


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