quarkusio/quarkus · error · IllegalStateException
RestClientProxy is closed
Error message
RestClientProxy is closed
What it means
Thrown by QuarkusProxyInvocationHandler.invoke when a method is called on a MicroProfile REST client proxy that has already been closed, or when the underlying JAX-RS Client that owns it is closed (e.g. a sub-resource whose parent client was closed). Quarkus marks the proxy closed and refuses further calls rather than operating on a dead client.
Source
Thrown at extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java:105
this.interceptorBindingsMap = Collections.emptyMap();
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (RestClientProxy.class.equals(method.getDeclaringClass())) {
return invokeRestClientProxyMethod(method);
}
// Autocloseable/Closeable
if (method.getName().equals("close") && (args == null || args.length == 0)) {
close();
return null;
}
// Check if this proxy is closed or the client itself is closed. The client may be closed if this proxy was a
// sub-resource and the resource client itself was closed.
if (closed.get() || client.isClosed()) {
closed.set(true);
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;
}
View on GitHub (pinned to e1c734241f)
Solutions
- Remove the close() call or obtain a new proxy (e.g. via QuarkusRestClientBuilder or CDI injection) instead of reusing the closed one
- Keep the parent client open as long as any sub-resource proxies derived from it are in use
- Inject the client with an appropriate CDI scope (e.g. @ApplicationScoped) so it isn't destroyed while still referenced
- Check application logic that closes clients in finally blocks when failures/retries may reuse them later
Example fix
// before
MyClient c = RestClientBuilder.newBuilder().baseUri(uri).build(MyClient.class);
Response r1 = c.get();
c.close();
Response r2 = c.get(); // IllegalStateException: RestClientProxy is closed
// after
try (MyClient c = RestClientBuilder.newBuilder().baseUri(uri).build(MyClient.class)) {
Response r1 = c.get();
Response r2 = c.get(); // reuse before closing
} Defensive patterns
Strategy: try-catch
Validate before calling
RestClientProxy proxy = (RestClientProxy) client;
if (proxy != null) {
try {
JaxRsClientImpl.class.getDeclaredMethod("isClosed");
} catch (NoSuchMethodException ignored) {
}
// Prefer tracking lifecycle yourself:
boolean usable = !closedFlag.get();
} Type guard
static boolean isUsable(MyClient c) { return c instanceof RestClientProxy && !closedProxies.contains(c); } Try / catch
try {
response = client.get();
} catch (IllegalStateException e) {
if (e.getMessage().contains("RestClientProxy is closed")) {
client = buildClient(); // recreate
response = client.get();
} else {
throw e;
}
} Prevention
- Only close clients/proxies when you are truly done with them and all derived sub-resources
- Prefer CDI-managed injection over manual lifecycle management
- Never cache proxies beyond the scope that created them
- Avoid close() in finally blocks if a retry path may reuse the proxy
When it happens
Trigger: Calling any @Path-annotated method on a REST client proxy after calling close() on it; using a sub-resource obtained from a parent client after the parent client/proxy was closed; invoking a proxy after its scope (e.g. @Dependent bean destroy or programmatic client shutdown) ended.
Common situations: Caching a REST client proxy in a long-lived object after its CDI scope ended; closing a parent client and then reusing sub-resources; calling close() in a finally block and then retrying the request; lifecycle changes after upgrading Quarkus where client scoping changed.
Related errors
- The Reactive REST Client needs to be built within the contex
- Dev services for ${request.getName()} requires a startable s
- All parameters have already been loaded, it is too late to c
- Internal error: An attempt was made to start an application
- Cannot reset a started application
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/309a259fd8af997d.
Report an issue: GitHub.