quarkusio/quarkus · error · BlockingOperationNotAllowedException

You have attempted to inject AuthzClient on a IO thread. Thi

Error message

You have attempted to inject AuthzClient on a IO thread.
This is not allowed when PolicyEnforcer is resolved dynamically as blocking operations are required.
Make sure you are injecting AuthzClient from a worker thread.

What it means

The Keycloak PEP throws BlockingOperationNotAllowedException when AuthzClient is injected (e.g. as a CDI bean into a resource) and resolved on a Vert.x IO/event-loop thread while the PolicyEnforcer is dynamically resolved, because obtaining it requires blocking calls that must not run on the IO thread.

Source

Thrown at extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java:112

            routingContext = Arc.container().instance(CurrentVertxRequest.class).get().getCurrent();
        }

        if (routingContext != null && routingContext.get(POLICY_ENFORCER) != null) {
            return routingContext.<PolicyEnforcer> get(POLICY_ENFORCER).getAuthzClient();
        } else if (BlockingOperationControl.isBlockingAllowed()) {
            OidcTenantConfig tenantConfig = routingContext == null ? null
                    : routingContext.get(OidcTenantConfig.class.getName());
            return resolver.resolvePolicyEnforcer(routingContext, tenantConfig)
                    .await().indefinitely()
                    .getAuthzClient();
        } else {
            if (resolver instanceof DefaultPolicyEnforcerResolver defaultResolver
                    && !defaultResolver.hasDynamicPolicyEnforcers()) {
                return defaultResolver.getStaticPolicyEnforcer(identity.getAttribute(TENANT_ID_ATTRIBUTE)).getAuthzClient();
            } else {
                // this shouldn't happen inside HTTP request as policy enforcer is in most cases accessible from context
                // and the Authz client itself is blocking so users can as well inject it when on the worker thread
                throw new BlockingOperationNotAllowedException("""
                        You have attempted to inject AuthzClient on a IO thread.
                        This is not allowed when PolicyEnforcer is resolved dynamically as blocking operations are required.
                        Make sure you are injecting AuthzClient from a worker thread.
                        """);
            }
        }
    }

    private Uni<CheckResult> checkPermissionInternal(RoutingContext routingContext, SecurityIdentity identity) {
        AccessTokenCredential credential = identity.getCredential(AccessTokenCredential.class);

        if (credential == null) {
            // SecurityIdentity has been created by the authentication mechanism other than quarkus-oidc
            return CheckResult.permit();
        }

        VertxHttpFacade httpFacade = new VertxHttpFacade(routingContext, credential.getToken(), resolver.getReadTimeout());
        return resolver.resolvePolicyEnforcer(routingContext, routingContext.get(OidcTenantConfig.class.getName()))

View on GitHub (pinned to e1c734241f)

Solutions

  1. Inject or access AuthzClient from a worker thread: annotate the endpoint with @Blocking or return a plain (blocking) response type.
  2. Obtain the AuthzClient lazily inside a Uni created on a worker thread (emitOn/Infrastructure) or use Mutiny's runSubscriptionOn.
  3. If dynamic enforcers are not needed, use static policy-enforcer configuration so the static resolver path (IO-thread safe) is used.

Example fix

// before
@Inject AuthzClient authzClient; // resolved on IO thread in reactive endpoint
@GET
public Uni<String> get() { ... }
// after
@GET
@Blocking
public String get() { authzClient.protection().resource().list(); ... }
Defensive patterns

Strategy: validation

Validate before calling

// guard injection points: only access AuthzClient off the event loop
if (Vertx.currentContext() != null && Vertx.currentContext().isEventLoopContext()) {
    throw new IllegalStateException("Inject AuthzClient from a worker thread (@Blocking or Uni emitOn)");
}

Try / catch

try {
    AuthzClient c = authzClient; // injected
    c.protection().resource().findById(id);
} catch (BlockingOperationNotAllowedException e) {
    log.error("Move AuthzClient usage to a worker thread (@Blocking) or use static policy enforcers", e);
    throw e;
}

Prevention

When it happens

Trigger: Injecting AuthzClient (or calling KeycloakPolicyEnforcerAuthorizer.getAuthzClient) from a method executing on the event-loop thread — e.g. a REST resource method returning Uni/Multi (reactive) without @Blocking, while dynamic policy enforcers are configured (per-tenant / dynamic policy enforcer resolver).

Common situations: Mixing reactive endpoints (returning Uni) with the policy-enforcer extension and injecting AuthzClient directly; enabling dynamic tenant/policy-enforcer resolution; moving from static to dynamic policy enforcer config without adjusting endpoint threading.

Related errors


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