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
- Create a new FrameworkHttpClientFactory instance instead of reusing the closed one.
- Track factory lifecycle: guard newHttpClient calls with a closed check or make the factory a scoped resource tied to the client's lifetime.
- 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
- Treat the factory as single-lifecycle: one factory per client, closed together.
- Track factory state in an AtomicBoolean and check before newHttpClient.
- Avoid sharing one factory across restart/teardown logic.
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
- Pulsar client has been closed, can not build LookupService w
- ServiceUrlProvider has already been initialized
- SASL authentication HTTP client is not started
- the PulsarTlsFactory passed to tlsFactory(...) has already b
- The log error handler cannot be changed once the appender is
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/8fa688eaf0950b7e.
Report an issue: GitHub.