apache/pulsar · error · IllegalStateException
SASL authentication HTTP client is not started
Error message
SASL authentication HTTP client is not started
What it means
AuthenticationSasl lazily creates a JAX-RS Client in start(); the HttpChallengeTransport.get() method reads that field into a local and throws IllegalStateException if it is still null. This means the plugin's HTTP-based SASL challenge exchange was invoked before the authentication plugin lifecycle started (or after it was closed). The library throws eagerly rather than sending a request with an unusable client.
Source
Thrown at pulsar-client-auth-sasl/src/main/java/org/apache/pulsar/client/impl/auth/AuthenticationSasl.java:250
/**
* A {@link HttpChallengeTransport} backed by this plugin's own JAX-RS {@link Client} (created in
* {@link #start()}) — the faithful transport for the admin-path SASL warmup, matching what
* {@code authenticationStage(...)} does today. Each round is a bodiless {@code GET} to the original URI.
* The JAX-RS client itself is created with default, unbounded timeouts, so the per-request {@code timeout}
* is enforced here by bounding the returned future ({@link CompletableFuture#orTimeout}) AND cancelling the
* underlying JAX-RS {@link Future} on timeout/failure — the request is bounded, so a peer that accepts the
* connection but never responds cannot leak an in-flight request (fd/socket exhaustion) across retries. A
* response that arrives after the future already timed out is closed rather than leaked.
*/
private final class JaxRsChallengeTransport implements HttpChallengeTransport {
@Override
public CompletableFuture<Result> get(URI uri, HttpAuthHeaders requestHeaders, Duration timeout) {
CompletableFuture<Result> future = new CompletableFuture<>();
try {
Client c = client;
if (c == null) {
throw new IllegalStateException("SASL authentication HTTP client is not started");
}
Builder builder = c.target(uri).request(MediaType.APPLICATION_JSON);
requestHeaders.asMap().forEach(builder::header);
Future<Response> responseFuture = builder.async().get(new InvocationCallback<Response>() {
@Override
public void completed(Response response) {
completeAndClose(future, response);
}
@Override
public void failed(Throwable throwable) {
future.completeExceptionally(throwable);
}
});
if (timeout != null && !timeout.isNegative() && !timeout.isZero()) {
future.orTimeout(timeout.toNanos(), TimeUnit.NANOSECONDS);
}
// Cancel the unbounded JAX-RS request on timeout/failure so its socket is released, not leaked.View on GitHub (pinned to 820761864e)
Solutions
- Call start() on the AuthenticationSasl instance before performing any authentication/challenge requests.
- Do not reuse a plugin instance after close(); create a new AuthenticationSasl (or new PulsarClient) instead.
- If constructing the plugin manually, replicate the full lifecycle: configure(...) then start() before get().
- Check for concurrent close/reuse races; synchronize shutdown so no get() can run after close().
Example fix
// before
AuthenticationSasl auth = new AuthenticationSasl();
auth.configure(authParamsString);
auth.get(uri, headers, timeout); // IllegalStateException: client not started
// after
AuthenticationSasl auth = new AuthenticationSasl();
auth.configure(authParamsString);
auth.start();
try {
auth.get(uri, headers, timeout);
} finally {
auth.close();
} Defensive patterns
Strategy: try-catch
Validate before calling
// Java: verify the plugin is started before use
// Field client is private; check lifecycle instead:
if (!started) { throw new IllegalStateException("Call start() before using AuthenticationSasl"); } Try / catch
try {
CompletableFuture<HttpChallengeTransport.Result> f = transport.get(uri, headers, timeout);
f.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof IllegalStateException
&& e.getCause().getMessage().contains("not started")) {
auth.start(); // restart and retry once
} else { throw e; }
} Prevention
- Always drive the plugin through PulsarClient/PulsarAdmin builders so lifecycle (configure/start/close) is managed for you.
- Never reuse a plugin or client instance after close(); build a new one.
- In tests, call start() in setup and close() in teardown of the auth plugin.
- Document/order code so authentication never races shutdown.
When it happens
Trigger: Calling AuthenticationSasl.get()/the challenge transport before start() has completed, using the plugin instance without initializing it (e.g. constructing it manually instead of via PulsarClient/AuthenticationProvider registry), or calling get() after close() nulled the client field (race with shutdown or reuse of a closed plugin).
Common situations: Hand-wiring the auth plugin in tests or embedded tools without calling start(); sharing a single AuthenticationSasl instance across a client that has been closed and reopened; a lifecycle ordering bug where authentication begins concurrently with/after close().
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Pulsar client has been closed, can not build LookupService w
- ServiceUrlProvider has already been initialized
- PulsarHttpClientFactory for ${clientInstanceId} is closed
- 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/9f1c843da87aa4ba.
Report an issue: GitHub.