{"record":{"id":"454cdf7d74444b12","repo":"apache/pulsar","slug":"no-usable-service-url-usetls-usetls-serviceur","errorCode":null,"errorMessage":"No usable service URL (useTls=${useTls}, serviceUrl=${serviceUrl}, serviceUrlTls=${serviceUrlTls})","messagePattern":"No usable service URL \\(useTls=(.+?), serviceUrl=(.+?), serviceUrlTls=(.+?)\\)","errorType":"exception","errorClass":"URISyntaxException","httpStatus":null,"severity":"error","filePath":"pulsar-client/src/main/java/org/apache/pulsar/client/impl/HandlerState.java","lineNumber":61,"sourceCode":"        Terminated, // Topic associated with this handler\n                    // has been terminated\n        Failed, // Handler is failed\n        RegisteringSchema, // Handler is registering schema\n        ProducerFenced, // The producer has been fenced by the broker\n    };\n\n    public HandlerState(PulsarClientImpl client, String topic) {\n        this.client = client;\n        this.topic = topic;\n        STATE_UPDATER.set(this, State.Uninitialized);\n    }\n\n    protected void setRedirectedClusterURI(String serviceUrl, String serviceUrlTls) throws URISyntaxException {\n        String url = client.conf.isUseTls() && StringUtils.isNotBlank(serviceUrlTls) ? serviceUrlTls : serviceUrl;\n        if (StringUtils.isBlank(url)) {\n            // e.g. a non-TLS client given a TLS-only endpoint (or vice versa). Surface a clear,\n            // catchable error rather than letting new URI(null) throw an NPE.\n            throw new URISyntaxException(String.valueOf(url),\n                    \"No usable service URL (useTls=\" + client.conf.isUseTls()\n                            + \", serviceUrl=\" + serviceUrl + \", serviceUrlTls=\" + serviceUrlTls + \")\");\n        }\n        this.redirectedClusterURI = new URI(url);\n    }\n\n    // moves the state to ready if it wasn't closed\n    protected boolean changeToReadyState() {\n        if (STATE_UPDATER.get(this) == State.Ready) {\n            return true;\n        }\n        return (STATE_UPDATER.compareAndSet(this, State.Uninitialized, State.Ready)\n                || STATE_UPDATER.compareAndSet(this, State.Connecting, State.Ready)\n                || STATE_UPDATER.compareAndSet(this, State.RegisteringSchema, State.Ready));\n    }\n\n    protected boolean compareAndSetState(State expect, State update) {\n        return STATE_UPDATER.compareAndSet(this, expect, update);","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-client/src/main/java/org/apache/pulsar/client/impl/HandlerState.java#L43-L79","documentation":"When a topic is migrated, the broker sends a service URL for the new cluster; setRedirectedClusterURI picks the TLS or non-TLS URL based on client config. If the selected URL is blank (e.g. a non-TLS client pointed at a TLS-only endpoint), it throws URISyntaxException with a descriptive reason instead of letting new URI(null) throw an opaque NPE.","triggerScenarios":"handleTopicMigrated receives migration metadata where the URL matching the client's useTls setting is null/empty: a TLS client on a topic migrated to a cluster with no serviceUrlTls, or a plain client on a TLS-only migration target.","commonSituations":"Cluster migration where the new cluster publishes only one of the two URLs; client TLS misconfiguration (useTls not matching available endpoint); partial broker configuration on the destination cluster.","solutions":["Enable TLS on the client (PulsarClientImpl builder useTls(true) / serviceUrl pulsar+ssl://) when the target only offers serviceUrlTls.","Fix the destination cluster configuration so it publishes both serviceUrl and serviceUrlTls.","Catch URISyntaxException in migration handling and fail over manually to the available URL."],"exampleFix":"// before\nPulsarClient client = PulsarClient.builder().serviceUrl(\"pulsar://old:6650\").build(); // TLS-only target\n// after\nPulsarClient client = PulsarClient.builder().serviceUrl(\"pulsar+ssl://old:6651\").enableTls(true).build();","handlingStrategy":"try-catch","validationCode":"// pre-check matching URL availability when handling migration metadata\nboolean usable = client.getConfiguration().isUseTls()\n    ? StringUtils.isNotBlank(serviceUrlTls) : StringUtils.isNotBlank(serviceUrl);","typeGuard":null,"tryCatchPattern":"try {\n  handler.handleTopicMigrated(...);\n} catch (URISyntaxException e) {\n  // no URL matching client TLS mode: reconnect to original or fail over manually\n}","preventionTips":["Ensure migrated clusters publish both pulsar:// and pulsar+ssl:// service URLs.","Keep client useTls consistent with the endpoints published by the cluster.","Handle topic-migration events explicitly in client wrappers."],"tags":["pulsar","topic-migration","tls","failover"],"backgroundTag":"missing-service-url","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"}