From e17c0282f117b5bb03990172c51bf57f8f4fea67 Mon Sep 17 00:00:00 2001 From: Paul Annesley Date: Mon, 30 Mar 2026 16:05:49 +1030 Subject: [PATCH] client-v2 HTTP auth: fix null password being sent as "null" string When no password is set on the v2 HTTP client, PASSWORD config returns null and Java string concatenation produces "default:null" instead of "default:" in the Authorization header. This causes authentication failures against ClickHouse servers expecting an empty password. e.g. the official docker image running with CLICKHOUSE_SKIP_USER_SETUP. Add a null check to treat missing passwords as empty string, matching the existing behavior in the v1 HTTP client. Add a WireMock-based test to verify the correct Authorization header is sent. --- .../api/internal/HttpAPIClientHelper.java | 3 ++ .../clickhouse/client/HttpTransportTests.java | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java index 76e2dec93..7ca25f473 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java @@ -601,6 +601,9 @@ private void addHeaders(HttpPost req, Map requestConfig) { } else if (ClientConfigProperties.HTTP_USE_BASIC_AUTH.getOrDefault(requestConfig).booleanValue()) { String user = ClientConfigProperties.USER.getOrDefault(requestConfig); String password = ClientConfigProperties.PASSWORD.getOrDefault(requestConfig); + if (password == null) { + password = ""; + } // Use as-is, no encoding allowed req.addHeader( HttpHeaders.AUTHORIZATION, diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index fc2d13a86..0bf88ea46 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -1003,6 +1003,40 @@ public void testBearerTokenAuth() throws Exception { } } + @Test(groups = { "integration" }) + public void testBasicAuthWithNoPassword() throws Exception { + WireMockServer mockServer = new WireMockServer(WireMockConfiguration + .options().port(9090).notifier(new ConsoleNotifier(false))); + mockServer.start(); + + try { + // Expected: "default:" with empty password, not "default:null" + String expectedAuth = "Basic " + Base64.getEncoder() + .encodeToString("default:".getBytes(StandardCharsets.UTF_8)); + + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withHeader("Authorization", WireMock.equalTo(expectedAuth)) + .willReturn(WireMock.aResponse() + .withHeader("X-ClickHouse-Summary", + "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build()); + + try (Client client = new Client.Builder() + .addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) + .compressServerResponse(false) + // no setPassword() call — password should default to empty, not "null" + .build()) { + + try (QueryResponse response = client.query("SELECT 1").get(1, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getReadBytes(), 10); + } catch (Exception e) { + Assert.fail("Basic auth with no password should send empty password, not 'null'", e); + } + } + } finally { + mockServer.stop(); + } + } + @Test(groups = { "integration" }) public void testJWTWithCloud() throws Exception { if (!isCloud()) {