apache/pulsar · error · org.apache.pulsar.client.impl.v5.PulsarClientException

${e.getMessage()}

Error message

${e.getMessage()}

What it means

PulsarClientBuilderV5.build() constructs the underlying v4 PulsarClientImpl, which validates configuration and resolves TLS/authentication eagerly. If that throws a v4 org.apache.pulsar.client.api.PulsarClientException, build() rethrows it as the v5 PulsarClientException with the same message and the original as cause. The builder remains retryable if the failure happened before the TLS factory was consumed.

Source

Thrown at pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/PulsarClientBuilderV5.java:119

            // TLS for the other. This path matters more than the v4 one: tlsFactory(...) and tlsPolicy(...)
            // exist only here, so it is the only builder that can reach the adopted-factory arm
            // deliberately, and it is what turns on the fail-fast probe whose failure handler closes the
            // factory.
            //
            // The copy is taken first, and applyAuthentication() then resolves into it, because that step
            // writes too: it puts the resolved plugin in one of the two authentication slots and folds a
            // bridged v4 plugin's certificate and key into CLIENT_DEFAULT. Run against the builder those
            // writes outlive the client — the next build() would keep the previous plugin in the other slot
            // and present the previous plugin's client certificate. clone() is shallow, so the policy map
            // has to be copied separately for the fold to land only on this client.
            if (clientConf.getTlsPolicyMap() != null) {
                clientConf.setTlsPolicyMap(new LinkedHashMap<>(clientConf.getTlsPolicyMap()));
            }
            applyAuthentication(clientConf);
            var v4Client = new PulsarClientImpl(clientConf);
            return new PulsarClientV5(v4Client, description, transactionTimeout);
        } catch (org.apache.pulsar.client.api.PulsarClientException e) {
            throw new PulsarClientException(e.getMessage(), e);
        } finally {
            // Whether the factory was consumed is read off the copy the client was given rather than
            // inferred from how the build ended: a build that failed after the framework took the instance
            // has spent it (initialized, and closed again on the way out) exactly as a successful one has,
            // while one that failed before — no serviceUrl, say — has not touched it and must leave the
            // builder able to retry with it.
            // The claim was taken up front so it could be atomic; this gives it back when the mark says
            // nothing consumed it.
            releaseTlsFactoryUnlessSpent(adopting, clientConf);
        }
    }

    /**
     * PIP-478: reject a second client built from a {@link PulsarTlsFactory} instance this builder has
     * already handed over.
     *
     * <p>Copying the configuration keeps each client's <em>composed</em> factory to itself, but an adopted
     * one is the caller's instance and the copy carries the same reference. Handing it to a second client

View on GitHub (pinned to 820761864e)

Solutions

  1. Inspect e.getMessage()/getCause() for the underlying v4 error and fix the configuration it names (serviceUrl, TLS paths, auth params).
  2. Ensure serviceUrl(String) is called before build() and uses pulsar:// or pulsar+ssl://.
  3. If the error mentions TLS material, verify the tlsPolicy/tlsFactory files exist and are readable.
  4. After fixing, rebuild from the same builder — it is still usable unless a tlsFactory was already adopted.

Example fix

// before
PulsarClient c = PulsarClient.builder().build(); // missing serviceUrl -> PulsarClientException
// after
PulsarClient c = PulsarClient.builder()
        .serviceUrl("pulsar://localhost:6650")
        .build();
Defensive patterns

Strategy: try-catch

Validate before calling

Objects.requireNonNull(serviceUrl, "serviceUrl must be set");
if (!serviceUrl.startsWith("pulsar://") && !serviceUrl.startsWith("pulsar+ssl://")) {
    throw new IllegalArgumentException("serviceUrl must be pulsar:// or pulsar+ssl://");
}
// plus: check any TLS keystore/truststore files exist and are readable before build()

Try / catch

try {
    client = builder.build();
} catch (PulsarClientException e) {
    LOG.error("client build failed: {} (cause: {})", e.getMessage(), e.getCause(), e);
    throw new IllegalStateException("Pulsar client configuration invalid", e);
}

Prevention

When it happens

Trigger: Calling build() with an invalid or missing serviceUrl, an unreachable or misconfigured TLS setup, an authentication plugin that fails to start, or any other v4 client construction error thrown from new PulsarClientImpl(clientConf).

Common situations: Forgot to call serviceUrl(); typo in the pulsar:// URL; keystore/truststore file paths that don't exist; auth plugin class name wrong so the client can't initialize; connecting to a TLS port with a plaintext URL.

Related errors


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