{"record":{"id":"9f1c843da87aa4ba","repo":"apache/pulsar","slug":"sasl-authentication-http-client-is-not-started","errorCode":null,"errorMessage":"SASL authentication HTTP client is not started","messagePattern":"SASL authentication HTTP client is not started","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"pulsar-client-auth-sasl/src/main/java/org/apache/pulsar/client/impl/auth/AuthenticationSasl.java","lineNumber":250,"sourceCode":"\n    /**\n     * A {@link HttpChallengeTransport} backed by this plugin's own JAX-RS {@link Client} (created in\n     * {@link #start()}) — the faithful transport for the admin-path SASL warmup, matching what\n     * {@code authenticationStage(...)} does today. Each round is a bodiless {@code GET} to the original URI.\n     * The JAX-RS client itself is created with default, unbounded timeouts, so the per-request {@code timeout}\n     * is enforced here by bounding the returned future ({@link CompletableFuture#orTimeout}) AND cancelling the\n     * underlying JAX-RS {@link Future} on timeout/failure — the request is bounded, so a peer that accepts the\n     * connection but never responds cannot leak an in-flight request (fd/socket exhaustion) across retries. A\n     * response that arrives after the future already timed out is closed rather than leaked.\n     */\n    private final class JaxRsChallengeTransport implements HttpChallengeTransport {\n        @Override\n        public CompletableFuture<Result> get(URI uri, HttpAuthHeaders requestHeaders, Duration timeout) {\n            CompletableFuture<Result> future = new CompletableFuture<>();\n            try {\n                Client c = client;\n                if (c == null) {\n                    throw new IllegalStateException(\"SASL authentication HTTP client is not started\");\n                }\n                Builder builder = c.target(uri).request(MediaType.APPLICATION_JSON);\n                requestHeaders.asMap().forEach(builder::header);\n                Future<Response> responseFuture = builder.async().get(new InvocationCallback<Response>() {\n                    @Override\n                    public void completed(Response response) {\n                        completeAndClose(future, response);\n                    }\n\n                    @Override\n                    public void failed(Throwable throwable) {\n                        future.completeExceptionally(throwable);\n                    }\n                });\n                if (timeout != null && !timeout.isNegative() && !timeout.isZero()) {\n                    future.orTimeout(timeout.toNanos(), TimeUnit.NANOSECONDS);\n                }\n                // Cancel the unbounded JAX-RS request on timeout/failure so its socket is released, not leaked.","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-client-auth-sasl/src/main/java/org/apache/pulsar/client/impl/auth/AuthenticationSasl.java#L232-L268","documentation":"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.","triggerScenarios":"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).","commonSituations":"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().","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()."],"exampleFix":"// before\nAuthenticationSasl auth = new AuthenticationSasl();\nauth.configure(authParamsString);\nauth.get(uri, headers, timeout); // IllegalStateException: client not started\n// after\nAuthenticationSasl auth = new AuthenticationSasl();\nauth.configure(authParamsString);\nauth.start();\ntry {\n    auth.get(uri, headers, timeout);\n} finally {\n    auth.close();\n}","handlingStrategy":"try-catch","validationCode":"// Java: verify the plugin is started before use\n// Field client is private; check lifecycle instead:\nif (!started) { throw new IllegalStateException(\"Call start() before using AuthenticationSasl\"); }","typeGuard":null,"tryCatchPattern":"try {\n    CompletableFuture<HttpChallengeTransport.Result> f = transport.get(uri, headers, timeout);\n    f.get(timeout.toMillis(), TimeUnit.MILLISECONDS);\n} catch (ExecutionException e) {\n    if (e.getCause() instanceof IllegalStateException\n            && e.getCause().getMessage().contains(\"not started\")) {\n        auth.start(); // restart and retry once\n    } else { throw e; }\n}","preventionTips":["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."],"tags":["lifecycle","sasl","illegal-state","initialization-order"],"backgroundTag":"auth-plugin-not-started","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}