apache/pulsar · error · IllegalStateException

PulsarHttpClientFactory for ${clientInstanceId} is closed

Error message

PulsarHttpClientFactory for ${clientInstanceId} is closed

What it means

FrameworkHttpClientFactory creates PulsarHttpClient instances sharing one event loop group and timer. Once close() has been called, the factory is marked closed and newHttpClient() refuses to hand out new clients holding onto the (now shutting down) shared resources, throwing this IllegalStateException under the factory lock.

Source

Thrown at pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/v5/FrameworkHttpClientFactory.java:129

     * @param clientInstanceId a stable id of the owning client, for logging
     */
    public FrameworkHttpClientFactory(Supplier<EventLoopGroup> eventLoopGroup, Supplier<Timer> timer,
            Supplier<NameResolver<InetAddress>> nameResolver, Supplier<PulsarTlsFactory> tlsFactory,
            ClientConfigurationData conf, String clientInstanceId) {
        this.eventLoopGroup = eventLoopGroup;
        this.timer = timer;
        this.nameResolver = nameResolver;
        this.tlsFactory = tlsFactory;
        this.conf = conf;
        this.clientInstanceId = clientInstanceId;
    }


    @Override
    public PulsarHttpClient newHttpClient(PulsarHttpClientConfig config) {
        synchronized (lock) {
            if (closed) {
                throw new IllegalStateException("PulsarHttpClientFactory for " + clientInstanceId + " is closed");
            }
            DefaultAsyncHttpClientConfig.Builder builder = baseBuilder(config);
            TlsHandle<SslContext> tlsSubscription = configureTls(builder, config);
            builder.setEventLoopGroup(eventLoopGroup.get());
            Timer sharedTimer = timer.get();
            if (sharedTimer != null) {
                builder.setNettyTimer(sharedTimer);
            }
            configureSocks5(builder);
            AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient(builder.build());
            // The self-deregistering runnable needs the client instance, which does not exist yet; capture it
            // through a holder so close() removes exactly this instance from the tracking set.
            FrameworkHttpClient[] ref = new FrameworkHttpClient[1];
            FrameworkHttpClient client = new FrameworkHttpClient(asyncHttpClient, config, resolveNameResolver(),
                    tlsSubscription, () -> deregister(ref[0]));
            ref[0] = client;
            openClients.add(client);
            return client;

View on GitHub (pinned to 820761864e)

Solutions

  1. Create a new FrameworkHttpClientFactory instance instead of reusing the closed one.
  2. Track factory lifecycle: guard newHttpClient calls with a closed check or make the factory a scoped resource tied to the client's lifetime.
  3. If the factory was closed unexpectedly, find the close() caller (e.g. a previous client's failure path) and fix the lifecycle ownership.

Example fix

// before
factory.close();
// ... later
PulsarHttpClient c = factory.newHttpClient(config); // throws
// after
factory.close();
factory = new FrameworkHttpClientFactory(clientInstanceId);
PulsarHttpClient c = factory.newHttpClient(config);
Defensive patterns

Strategy: try-catch

Validate before calling

boolean usable = false;
try {
    java.lang.reflect.Field f = factory.getClass().getDeclaredField("closed");
    f.setAccessible(true); // or track closed state yourself
    usable = !f.getBoolean(factory);
} catch (Exception ignored) {}
// preferred: keep your own AtomicBoolean mirroring factory.close()

Type guard

static boolean isOpen(FrameworkHttpClientFactory f, java.util.concurrent.atomic.AtomicBoolean ownedClosed) {
    return !ownedClosed.get();
}

Try / catch

try {
    client = factory.newHttpClient(config);
} catch (IllegalStateException e) {
    if (e.getMessage() != null && e.getMessage().contains("is closed")) {
        factory = createNewFactory(); // recreate and retry once
        client = factory.newHttpClient(config);
    } else throw e;
}

Prevention

When it happens

Trigger: Calling factory.newHttpClient(config) after factory.close() has run. Seen in production via the client bootstrap path and in tests such as testFactoryCloseClosesInstancesAndRejectsNew.

Common situations: Creating a new PulsarClient with a factory that was already shut down during application restart logic; reusing a singleton factory after a failed client initialization closed it; race between a shutdown hook and request-handling code creating clients.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/8fa688eaf0950b7e. Report an issue: GitHub.