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
- Change the return type to primitive boolean for synchronous checks
- Change the return type to Uni<Boolean> for reactive checks (SmallRye Mutiny)
- If using CompletionStage, convert it: Uni.createFrom().completionStage(...)
- 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
- Use primitive boolean, never boxed Boolean/Optional
- Use Uni<Boolean> (Mutiny), not CompletionStage, for async checks
- Type-check @PermissionChecker methods in review and convention tests
- Follow io.quarkus.security.PermissionChecker documentation examples
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
- Private method '' cannot be annotated with the @PermissionCh
- Static method '' cannot be annotated with the @PermissionChe
- @PermissionChecker annotation placed on the '%s' attribute '
- @PermissionChecker annotation instance placed on the '%s' re
- Detected two @PermissionChecker annotations with same value
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/36c05c812648f8a1.
Report an issue: GitHub.