diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java
new file mode 100644
index 0000000000000..2eba8ce96542c
--- /dev/null
+++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java
@@ -0,0 +1,296 @@
+package io.quarkus.restclient.runtime;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import jakarta.enterprise.context.spi.CreationalContext;
+import jakarta.enterprise.inject.spi.BeanManager;
+import jakarta.enterprise.inject.spi.CDI;
+import jakarta.enterprise.inject.spi.InterceptionType;
+import jakarta.enterprise.inject.spi.Interceptor;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.ResponseProcessingException;
+import jakarta.ws.rs.ext.ParamConverter;
+import jakarta.ws.rs.ext.ParamConverterProvider;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.client.jaxrs.ResteasyClient;
+import org.jboss.resteasy.microprofile.client.ExceptionMapping;
+import org.jboss.resteasy.microprofile.client.InvocationContextImpl;
+import org.jboss.resteasy.microprofile.client.RestClientProxy;
+import org.jboss.resteasy.microprofile.client.header.ClientHeaderFillingException;
+
+/**
+ * Quarkus version of {@link org.jboss.resteasy.microprofile.client.ProxyInvocationHandler} retaining the ability to
+ * create a custom interceptor chain and invoke it manually.
+ *
+ * This is needed due to changes in https://github.com/resteasy/resteasy-microprofile/pull/182
+ *
+ * In theory, it could be improved by pre-generating proxies for {@code @RegisterRestClient} interfaces and registering
+ * them as standard beans with all their interceptor bindings.
+ */
+public class QuarkusProxyInvocationHandler implements InvocationHandler {
+
+ private static final Logger LOGGER = Logger.getLogger(QuarkusProxyInvocationHandler.class);
+ public static final Type[] NO_TYPES = {};
+
+ private final Object target;
+
+ private final Set providerInstances;
+
+ private final Map> interceptorChains;
+
+ private final ResteasyClient client;
+
+ private final CreationalContext> creationalContext;
+
+ private final AtomicBoolean closed;
+
+ public QuarkusProxyInvocationHandler(final Class> restClientInterface,
+ final Object target,
+ final Set providerInstances,
+ final ResteasyClient client, final BeanManager beanManager) {
+ this.target = target;
+ this.providerInstances = providerInstances;
+ this.client = client;
+ this.closed = new AtomicBoolean();
+ if (beanManager != null) {
+ this.creationalContext = beanManager.createCreationalContext(null);
+ this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface);
+ } else {
+ this.creationalContext = null;
+ this.interceptorChains = Collections.emptyMap();
+ }
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if (RestClientProxy.class.equals(method.getDeclaringClass())) {
+ return invokeRestClientProxyMethod(proxy, method, args);
+ }
+ // Autocloseable/Closeable
+ if (method.getName().equals("close") && (args == null || args.length == 0)) {
+ close();
+ return null;
+ }
+ if (closed.get()) {
+ throw new IllegalStateException("RestClientProxy is closed");
+ }
+
+ boolean replacementNeeded = false;
+ Object[] argsReplacement = args != null ? new Object[args.length] : null;
+ Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+
+ if (args != null) {
+ for (Object p : providerInstances) {
+ if (p instanceof ParamConverterProvider) {
+
+ int index = 0;
+ for (Object arg : args) {
+ // ParamConverter's are not allowed to be passed null values. If we have a null value do not process
+ // it through the provider.
+ if (arg == null) {
+ continue;
+ }
+
+ if (parameterAnnotations[index].length > 0) { // does a parameter converter apply?
+ ParamConverter> converter = ((ParamConverterProvider) p).getConverter(arg.getClass(), null,
+ parameterAnnotations[index]);
+ if (converter != null) {
+ Type[] genericTypes = getGenericTypes(converter.getClass());
+ if (genericTypes.length == 1) {
+
+ // minimum supported types
+ switch (genericTypes[0].getTypeName()) {
+ case "java.lang.String":
+ @SuppressWarnings("unchecked")
+ ParamConverter stringConverter = (ParamConverter) converter;
+ argsReplacement[index] = stringConverter.toString((String) arg);
+ replacementNeeded = true;
+ break;
+ case "java.lang.Integer":
+ @SuppressWarnings("unchecked")
+ ParamConverter intConverter = (ParamConverter) converter;
+ argsReplacement[index] = intConverter.toString((Integer) arg);
+ replacementNeeded = true;
+ break;
+ case "java.lang.Boolean":
+ @SuppressWarnings("unchecked")
+ ParamConverter boolConverter = (ParamConverter) converter;
+ argsReplacement[index] = boolConverter.toString((Boolean) arg);
+ replacementNeeded = true;
+ break;
+ default:
+ continue;
+ }
+ }
+ }
+ } else {
+ argsReplacement[index] = arg;
+ }
+ index++;
+ }
+ }
+ }
+ }
+
+ if (replacementNeeded) {
+ args = argsReplacement;
+ }
+
+ List chain = interceptorChains.get(method);
+ if (chain != null) {
+ // Invoke business method interceptors
+ return new InvocationContextImpl(target, method, args, chain).proceed();
+ } else {
+ try {
+ return method.invoke(target, args);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof CompletionException) {
+ cause = cause.getCause();
+ }
+ if (cause instanceof ExceptionMapping.HandlerException) {
+ ((ExceptionMapping.HandlerException) cause).mapException(method);
+ // no applicable exception mapper found or applicable mapper returned null
+ return null;
+ }
+ if (cause instanceof ResponseProcessingException) {
+ ResponseProcessingException rpe = (ResponseProcessingException) cause;
+ cause = rpe.getCause();
+ if (cause instanceof RuntimeException) {
+ throw cause;
+ }
+ } else {
+ if (cause instanceof ProcessingException &&
+ cause.getCause() instanceof ClientHeaderFillingException) {
+ throw cause.getCause().getCause();
+ }
+ if (cause instanceof RuntimeException) {
+ throw cause;
+ }
+ }
+ throw e;
+ }
+ }
+ }
+
+ private Object invokeRestClientProxyMethod(Object proxy, Method method, Object[] args) {
+ switch (method.getName()) {
+ case "getClient":
+ return client;
+ case "close":
+ close();
+ return null;
+ default:
+ throw new IllegalStateException("Unsupported RestClientProxy method: " + method);
+ }
+ }
+
+ private void close() {
+ if (closed.compareAndSet(false, true)) {
+ if (creationalContext != null) {
+ creationalContext.release();
+ }
+ client.close();
+ }
+ }
+
+ private Type[] getGenericTypes(Class> aClass) {
+ Type[] genericInterfaces = aClass.getGenericInterfaces();
+ Type[] genericTypes = NO_TYPES;
+ for (Type genericInterface : genericInterfaces) {
+ if (genericInterface instanceof ParameterizedType) {
+ genericTypes = ((ParameterizedType) genericInterface).getActualTypeArguments();
+ }
+ }
+ return genericTypes;
+ }
+
+ private static List getBindings(Annotation[] annotations, BeanManager beanManager) {
+ if (annotations.length == 0) {
+ return Collections.emptyList();
+ }
+ List bindings = new ArrayList<>();
+ for (Annotation annotation : annotations) {
+ if (beanManager.isInterceptorBinding(annotation.annotationType())) {
+ bindings.add(annotation);
+ }
+ }
+ return bindings;
+ }
+
+ private static BeanManager getBeanManager(Class> restClientInterface) {
+ try {
+ CDI current = CDI.current();
+ return current != null ? current.getBeanManager() : null;
+ } catch (IllegalStateException e) {
+ LOGGER.warnf("CDI container is not available - interceptor bindings declared on %s will be ignored",
+ restClientInterface.getSimpleName());
+ return null;
+ }
+ }
+
+ private static Map> initInterceptorChains(
+ BeanManager beanManager, CreationalContext> creationalContext, Class> restClientInterface) {
+
+ Map> chains = new HashMap<>();
+ // Interceptor as a key in a map is not entirely correct (custom interceptors) but should work in most cases
+ Map, Object> interceptorInstances = new HashMap<>();
+
+ List classLevelBindings = getBindings(restClientInterface.getAnnotations(), beanManager);
+
+ for (Method method : restClientInterface.getMethods()) {
+ if (method.isDefault() || Modifier.isStatic(method.getModifiers())) {
+ continue;
+ }
+ List methodLevelBindings = getBindings(method.getAnnotations(), beanManager);
+
+ if (!classLevelBindings.isEmpty() || !methodLevelBindings.isEmpty()) {
+
+ Annotation[] interceptorBindings = merge(methodLevelBindings, classLevelBindings);
+
+ List> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE,
+ interceptorBindings);
+ if (!interceptors.isEmpty()) {
+ List chain = new ArrayList<>();
+ for (Interceptor> interceptor : interceptors) {
+ chain.add(new InvocationContextImpl.InterceptorInvocation(interceptor,
+ interceptorInstances.computeIfAbsent(interceptor,
+ i -> beanManager.getReference(i, i.getBeanClass(), creationalContext))));
+ }
+ chains.put(method, chain);
+ }
+ }
+ }
+ return chains.isEmpty() ? Collections.emptyMap() : chains;
+ }
+
+ private static Annotation[] merge(List methodLevelBindings, List classLevelBindings) {
+ Set> types = methodLevelBindings.stream()
+ .map(a -> a.annotationType())
+ .collect(Collectors.toSet());
+ List merged = new ArrayList<>(methodLevelBindings);
+ for (Annotation annotation : classLevelBindings) {
+ if (!types.contains(annotation.annotationType())) {
+ merged.add(annotation);
+ }
+ }
+ return merged.toArray(new Annotation[] {});
+ }
+
+}
diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java
index 3c26f09ba35e9..ead9b44944922 100644
--- a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java
+++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java
@@ -73,7 +73,6 @@
import org.jboss.resteasy.microprofile.client.DefaultResponseExceptionMapper;
import org.jboss.resteasy.microprofile.client.ExceptionMapping;
import org.jboss.resteasy.microprofile.client.MethodInjectionFilter;
-import org.jboss.resteasy.microprofile.client.ProxyInvocationHandler;
import org.jboss.resteasy.microprofile.client.RestClientListeners;
import org.jboss.resteasy.microprofile.client.RestClientProxy;
import org.jboss.resteasy.microprofile.client.async.AsyncInterceptorRxInvokerProvider;
@@ -363,7 +362,7 @@ public T build(Class aClass, ClientHttpEngine httpEngine)
final BeanManager beanManager = getBeanManager();
T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces,
- new ProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client));
+ new QuarkusProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client, beanManager));
ClientHeaderProviders.registerForClass(aClass, proxy, beanManager);
return proxy;
}