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

  1. Remove the close() call or obtain a new proxy (e.g. via QuarkusRestClientBuilder or CDI injection) instead of reusing the closed one
  2. Keep the parent client open as long as any sub-resource proxies derived from it are in use
  3. Inject the client with an appropriate CDI scope (e.g. @ApplicationScoped) so it isn't destroyed while still referenced
  4. 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

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


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