quarkusio/quarkus · error · IllegalStateException

@ClientExceptionMapper is only supported on static methods o

Error message

@ClientExceptionMapper is only supported on static methods of REST Client interfaces that take 'jakarta.ws.rs.core.Response' as a single parameter and return 'java.lang.RuntimeException'. Offending instance is '${className}#${methodName}'

What it means

@ClientExceptionMapper marks a static method in a REST client interface that maps a failed Response to a RuntimeException. Quarkus enforces a fixed signature: exactly one parameter of type jakarta.ws.rs.core.Response and a return type of java.lang.RuntimeException (or subclass). This IllegalStateException is thrown at build time when the annotated method deviates from that contract.

Source

Thrown at extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/ClientExceptionMapperHandler.java:102

                try {
                    boolean returnsRuntimeException = RuntimeException.class.isAssignableFrom(
                            Class.forName(returnTypeClassName, false, Thread.currentThread().getContextClassLoader()));
                    if (returnsRuntimeException) {
                        isValid = true;
                    }
                } catch (ClassNotFoundException ignored) {

                }
            }
        }
        if (!isValid) {
            String message = DotNames.CLIENT_EXCEPTION_MAPPER
                    + " is only supported on static methods of REST Client interfaces that take 'jakarta.ws.rs.core.Response' as a single parameter and return 'java.lang.RuntimeException'.";
            if (targetMethod != null) {
                message += " Offending instance is '" + targetMethod.declaringClass().name().toString() + "#"
                        + targetMethod.name() + "'";
            }
            throw new IllegalStateException(message);
        }

        int priority = Priorities.USER;
        AnnotationValue priorityAnnotationValue = instance.value("priority");
        if (priorityAnnotationValue != null) {
            priority = priorityAnnotationValue.asInt();
        }

        ClassInfo restClientInterfaceClassInfo = targetMethod.declaringClass();
        String generatedClassName = getGeneratedClassName(targetMethod);
        final MethodInfo target = targetMethod;
        final int finalPriority = priority;
        gizmo.class_(generatedClassName, cc -> {
            cc.implements_(ResteasyReactiveResponseExceptionMapper.class);
            cc.defaultConstructor();
            cc.method("toThrowable", mc -> {
                mc.returning(Throwable.class);
                ParamVar response = mc.parameter("response", Response.class);

View on GitHub (pinned to e1c734241f)

Solutions

  1. Change the method to return RuntimeException (or a subclass) and accept exactly one jakarta.ws.rs.core.Response parameter
  2. Make the method static and keep it inside the REST client interface
  3. Wrap checked exceptions in a RuntimeException subclass before returning
  4. If you need richer context, capture it in fields/closure-like static state or use ClientRedirectHandler-style alternatives

Example fix

// before
@ClientExceptionMapper
IOException map(Response r) { return new IOException(); } // wrong return type
// after
@ClientExceptionMapper
static RuntimeException map(Response r) {
    if (r.getStatus() == 404) return new NotFoundException();
    return new InternalServerErrorException();
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate signature before annotating:
// static, one jakarta.ws.rs.core.Response param, returns RuntimeException
static boolean validMapperSignature(Method m) {
    return Modifier.isStatic(m.getModifiers())
        && m.getParameterCount() == 1
        && m.getParameterTypes()[0] == jakarta.ws.rs.core.Response.class
        && RuntimeException.class.isAssignableFrom(m.getReturnType());
}

Try / catch

try {
    handler.generateResponseExceptionMapper(classResult, instance);
} catch (IllegalStateException e) {
    if (e.getMessage().contains("ClientExceptionMapper")) { /* correct signature per message */ }
    throw e;
}

Prevention

When it happens

Trigger: @ClientExceptionMapper method that: is not static; takes zero, multiple, or non-Response parameters; returns Throwable/Exception/IOException instead of RuntimeException; is declared outside a REST client interface.

Common situations: Returning a custom checked exception; adding extra convenience parameters (e.g. URI or method) not allowed here; copy-pasting a @ServerExceptionMapper (which allows more signatures) into a client interface.

Related errors


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