From 540e1308fa68f8b3b7f231a392a03e305dd3d2d2 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 17 Aug 2026 14:28:44 -0700 Subject: [PATCH 1/2] Added custom header tests --- clickhouse-http-client/pom.xml | 2 +- .../http/config/ClickHouseHttpOption.java | 6 ++ .../http/ApacheHttpConnectionImplTest.java | 52 ++++++++++++++++ clickhouse-jdbc/pom.xml | 6 ++ .../jdbc/ClickHouseConnectionTest.java | 62 +++++++++++++++++++ client-v2/pom.xml | 2 +- .../client/api/http/ClickHouseHttpProto.java | 5 ++ jdbc-v2/pom.xml | 2 +- pom.xml | 1 + 9 files changed, 135 insertions(+), 3 deletions(-) diff --git a/clickhouse-http-client/pom.xml b/clickhouse-http-client/pom.xml index 2d2accbf2..a9da2db8a 100644 --- a/clickhouse-http-client/pom.xml +++ b/clickhouse-http-client/pom.xml @@ -87,7 +87,7 @@ org.wiremock wiremock-standalone - 3.13.0 + ${wiremock.version} test diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/config/ClickHouseHttpOption.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/config/ClickHouseHttpOption.java index 58e2f76f2..9b435fa24 100644 --- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/config/ClickHouseHttpOption.java +++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/config/ClickHouseHttpOption.java @@ -116,6 +116,12 @@ public enum ClickHouseHttpOption implements ClickHouseOption { */ USE_BASIC_AUTHENTICATION("http_use_basic_auth", true, "Whether to use basic authentication."); + /** + * Replica tag header used by a proxy to route a request to a specific replica. + * ClickHouse Cloud feature only. + */ + public static final String HEADER_REPLICA_TAG = "X-ClickHouse-Replica-Tag"; + private final String key; private final Serializable defaultValue; private final Class clazz; diff --git a/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ApacheHttpConnectionImplTest.java b/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ApacheHttpConnectionImplTest.java index 7a5e99de4..a71cc7ae0 100644 --- a/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ApacheHttpConnectionImplTest.java +++ b/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ApacheHttpConnectionImplTest.java @@ -14,6 +14,7 @@ import com.clickhouse.client.http.config.ClickHouseHttpOption; import com.clickhouse.client.http.config.HttpConnectionProvider; import com.clickhouse.config.ClickHouseOption; +import com.clickhouse.data.ClickHouseFormat; import com.clickhouse.data.ClickHouseUtils; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; @@ -77,6 +78,57 @@ protected Map getClientOptions() { HttpConnectionProvider.APACHE_HTTP_CLIENT); } + @Test(groups = { "unit" }, dataProvider = "replicaTags") + public void testCustomHeadersRouteToReplica(String replicaTag) throws Exception { + String host = "replica-router.clickhouse.test"; + String expectedReplica = "replica-for-requested-tag"; + String otherReplicaTag = "other-" + replicaTag; + WireMockServer mockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + mockServer.start(); + try { + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withHeader(ClickHouseHttpOption.HEADER_REPLICA_TAG, WireMock.equalTo(replicaTag)) + .withHeader("Host", WireMock.equalTo(host)) + .withRequestBody(WireMock.matching("(?is)select\\s+hostname\\(\\).*")) + .willReturn(WireMock.ok("hostname()\nString\n" + expectedReplica + "\n")) + .build()); + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withHeader(ClickHouseHttpOption.HEADER_REPLICA_TAG, WireMock.equalTo(otherReplicaTag)) + .withHeader("Host", WireMock.equalTo(host)) + .withRequestBody(WireMock.matching("(?is)select\\s+hostname\\(\\).*")) + .willReturn(WireMock.ok("hostname()\nString\nother-replica\n")) + .build()); + + Map options = new HashMap<>(); + options.put(ClickHouseHttpOption.CONNECTION_PROVIDER, HttpConnectionProvider.APACHE_HTTP_CLIENT); + options.put(ClickHouseClientOption.COMPRESS, false); + options.put(ClickHouseHttpOption.CUSTOM_HEADERS, + ClickHouseHttpOption.HEADER_REPLICA_TAG + "=" + replicaTag + ",Host=" + host); + + try (ClickHouseClient client = ClickHouseClient.builder().config(new ClickHouseConfig(options)).build(); + ClickHouseResponse response = client.read("http://localhost:" + mockServer.port()) + .format(ClickHouseFormat.TabSeparatedWithNamesAndTypes) + .query("select hostname()").executeAndWait()) { + Assert.assertEquals(response.firstRecord().getValue(0).asString(), expectedReplica); + } + + mockServer.verify(WireMock.postRequestedFor(WireMock.anyUrl()) + .withHeader(ClickHouseHttpOption.HEADER_REPLICA_TAG, WireMock.equalTo(replicaTag)) + .withHeader("Host", WireMock.equalTo(host))); + } finally { + mockServer.stop(); + } + } + + @DataProvider(name = "replicaTags") + public static Object[][] replicaTags() { + return new Object[][] { + { "550e8400-e29b-41d4-a716-446655440000" }, + { "replica-primary" }, + { "replica=primary" } + }; + } + @Test(groups = { "integration" }) public void testConnection() throws Exception { ClickHouseNode server = getServer(ClickHouseProtocol.HTTP); diff --git a/clickhouse-jdbc/pom.xml b/clickhouse-jdbc/pom.xml index 68e19d4d5..c4c56727f 100644 --- a/clickhouse-jdbc/pom.xml +++ b/clickhouse-jdbc/pom.xml @@ -96,6 +96,12 @@ testng test + + org.wiremock + wiremock-standalone + ${wiremock.version} + test + com.mysql mysql-connector-j diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseConnectionTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseConnectionTest.java index 04074f84a..61dc71bf2 100644 --- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseConnectionTest.java +++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseConnectionTest.java @@ -13,14 +13,20 @@ import com.clickhouse.client.ClickHouseRequest; import com.clickhouse.client.ClickHouseServerForTest; import com.clickhouse.client.config.ClickHouseClientOption; +import com.clickhouse.client.http.config.ClickHouseHttpOption; import com.clickhouse.data.ClickHouseCompression; +import com.clickhouse.data.ClickHouseFormat; import com.clickhouse.data.ClickHouseUtils; import com.clickhouse.data.value.UnsignedByte; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import org.testng.Assert; import org.testng.SkipException; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class ClickHouseConnectionTest extends JdbcIntegrationTest { @@ -37,6 +43,62 @@ public ClickHouseConnection newConnection(Properties properties) throws SQLExcep return (ClickHouseConnection) newDataSource(properties).getConnection(); } + @Test(groups = "unit", dataProvider = "replicaTags") + public void testCustomHeadersRouteToReplica(String replicaTag) throws Exception { + String host = "replica-router.clickhouse.test"; + String expectedReplica = "replica-for-requested-tag"; + String otherReplicaTag = "other-" + replicaTag; + WireMockServer mockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + mockServer.start(); + try { + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withHeader(ClickHouseHttpOption.HEADER_REPLICA_TAG, WireMock.equalTo(replicaTag)) + .withHeader("Host", WireMock.equalTo(host)) + .withRequestBody(WireMock.matching("(?is)select\\s+hostname\\(\\).*")) + .willReturn(WireMock.ok("hostname()\nString\n" + expectedReplica + "\n")) + .build()); + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withHeader(ClickHouseHttpOption.HEADER_REPLICA_TAG, WireMock.equalTo(otherReplicaTag)) + .withHeader("Host", WireMock.equalTo(host)) + .withRequestBody(WireMock.matching("(?is)select\\s+hostname\\(\\).*")) + .willReturn(WireMock.ok("hostname()\nString\nother-replica\n")) + .build()); + + Properties properties = new Properties(); + properties.setProperty(ClickHouseClientOption.SERVER_TIME_ZONE.getKey(), "UTC"); + properties.setProperty(ClickHouseClientOption.SERVER_VERSION.getKey(), "25.8"); + properties.setProperty(ClickHouseClientOption.COMPRESS.getKey(), Boolean.FALSE.toString()); + properties.setProperty(ClickHouseClientOption.FORMAT.getKey(), + ClickHouseFormat.TabSeparatedWithNamesAndTypes.name()); + properties.setProperty(ClickHouseHttpOption.CUSTOM_HEADERS.getKey(), + ClickHouseHttpOption.HEADER_REPLICA_TAG + "=" + replicaTag + ",Host=" + host); + + String url = "jdbc:clickhouse:http://localhost:" + mockServer.port() + "?clickhouse.jdbc.v1=true"; + try (Connection connection = new ClickHouseDataSource(url, properties).getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select hostname()")) { + Assert.assertTrue(resultSet.next()); + Assert.assertEquals(resultSet.getString(1), expectedReplica); + Assert.assertFalse(resultSet.next()); + } + + mockServer.verify(WireMock.postRequestedFor(WireMock.anyUrl()) + .withHeader(ClickHouseHttpOption.HEADER_REPLICA_TAG, WireMock.equalTo(replicaTag)) + .withHeader("Host", WireMock.equalTo(host))); + } finally { + mockServer.stop(); + } + } + + @DataProvider(name = "replicaTags") + public static Object[][] replicaTags() { + return new Object[][] { + { "550e8400-e29b-41d4-a716-446655440000" }, + { "replica-primary" }, + { "replica=primary" } + }; + } + @Test(groups = "integration") public void testCentralizedConfiguration() throws SQLException { if (isCloud()) return; //TODO: testCentralizedConfiguration - Revisit, see: https://github.com/ClickHouse/clickhouse-java/issues/1747 diff --git a/client-v2/pom.xml b/client-v2/pom.xml index 3836a8c6a..1d9e01327 100644 --- a/client-v2/pom.xml +++ b/client-v2/pom.xml @@ -137,7 +137,7 @@ org.wiremock wiremock-standalone - 3.13.0 + ${wiremock.version} test diff --git a/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java b/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java index d2e30f48b..860f74ca6 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/http/ClickHouseHttpProto.java @@ -52,6 +52,11 @@ public class ClickHouseHttpProto { public static final String HEADER_SSL_CERT_AUTH = "x-clickhouse-ssl-certificate-auth"; + /** + * Replica tag used by a proxy to route a request to a specific replica. + */ + public static final String HEADER_REPLICA_TAG = "X-ClickHouse-Replica-Tag"; + /** * Query parameter to specify the query ID. */ diff --git a/jdbc-v2/pom.xml b/jdbc-v2/pom.xml index 3f0989dee..be33c5512 100644 --- a/jdbc-v2/pom.xml +++ b/jdbc-v2/pom.xml @@ -105,7 +105,7 @@ org.wiremock wiremock-standalone - 3.13.0 + ${wiremock.version} test diff --git a/pom.xml b/pom.xml index 7e402d341..5ba91b64b 100644 --- a/pom.xml +++ b/pom.xml @@ -106,6 +106,7 @@ 1.21.3 7.5.1 + 3.13.0 3.1.4 8.1.0 From 8ae294d85de09be81713c2c314a2ccdc3a4d2386 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 17 Aug 2026 21:26:46 -0700 Subject: [PATCH 2/2] Added documentation for custom header setup and section about sticky session in Cloud --- docs/clickhouse-docs/client.mdx | 123 ++++++++++++++++++++++++++++---- docs/clickhouse-docs/jdbc.mdx | 113 +++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 14 deletions(-) diff --git a/docs/clickhouse-docs/client.mdx b/docs/clickhouse-docs/client.mdx index 6899c68bd..2b5897183 100644 --- a/docs/clickhouse-docs/client.mdx +++ b/docs/clickhouse-docs/client.mdx @@ -216,9 +216,9 @@ Configuration is defined during client creation. See `com.clickhouse.client.api. | Method | Arguments | Description | Default | Key | |--------|-----------|-------------|---------|-----| | `setHttpCookiesEnabled(boolean enabled)` | `enabled` - flag to enable/disable | Set if HTTP cookies should be remembered and sent to server back. | - | - | -| `httpHeader(String key, String value)` | `key` - HTTP header key
`value` - string value | Sets value for a single HTTP header. Previous value is overridden. | `none` | `none` | -| `httpHeader(String key, Collection values)` | `key` - HTTP header key
`values` - list of string values | Sets values for a single HTTP header. Previous value is overridden. | `none` | `none` | -| `httpHeaders(Map headers)` | `headers` - map with HTTP headers | Sets multiple HTTP header values at a time. | `none` | `none` | +| `httpHeader(String key, String value)` | `key` - HTTP header key
`value` - string value | Sets value for a single HTTP header. Previous value is overridden. | `none` | `http_header_*` | +| `httpHeader(String key, Collection values)` | `key` - HTTP header key
`values` - list of string values | Sets values for a single HTTP header. Previous value is overridden. | `none` | `http_header_*` | +| `httpHeaders(Map headers)` | `headers` - map with HTTP headers | Sets multiple HTTP header values at a time. | `none` | `http_header_*` | | `useHttpFormDataForQuery(boolean enable)` | `enable` - flag to enable/disable | Sets whether query parameters should be sent as HTTP form data in the request body instead of the URL. Works only with server-side compression. If client-level compression is enabled, it will be disabled for query requests with parameters, as each parameter is sent as multipart content. | `false` | `client.http.use_form_request_for_query` | @@ -324,18 +324,79 @@ Server side settings can be set on the client level once while creation (see `se ``` ⚠️ When options are set via `setOption` method (either the `Client.Builder` or operation settings class) then server settings name should be prefixed with `clickhouse_setting_`. The `com.clickhouse.client.api.ClientConfigProperties#serverSetting()` may be handy in this case. -### Custom HTTP Header +### Custom HTTP Headers {#custom-http-headers} + +Custom HTTP headers can be set for all operations (client level) or a single one (operation level). Operation settings override client-level headers with the same name. + +**Single header** -Custom HTTP headers can be set for all operations (client level) or a single one (operation level). ```java showLineNumbers +Client client = new Client.Builder() + .addEndpoint("https://clickhouse-cloud-instance:8443/") + .setUsername(user) + .setPassword(password) + .httpHeader("X-ClickHouse-Quota", "test") + .build(); QuerySettings settings = new QuerySettings() - .httpHeader(HttpHeaders.REFERER, clientReferer) - .setQueryId(qId); + .httpHeader(HttpHeaders.REFERER, clientReferer); +``` + +**Multiple headers** + +Use repeated `httpHeader` calls, or `httpHeaders(Map)`: + +```java showLineNumbers +Map headers = new HashMap<>(); +headers.put("X-ClickHouse-Quota", "test"); +headers.put("X-ClickHouse-Test", "test"); + +Client client = new Client.Builder() + .addEndpoint("https://clickhouse-cloud-instance:8443/") + .setUsername(user) + .setPassword(password) + .httpHeaders(headers) + .build(); +``` + +**Multiple values for one header** + +```java showLineNumbers +clientBuilder.httpHeader("X-Forwarded-For", Arrays.asList("1.2.3.4", "5.6.7.8")); +``` + +When options are set via `setOption` (on `Client.Builder` or an operation settings class), prefix the header name with `http_header_`. `ClientConfigProperties.httpHeader(String)` builds that key: + +```java showLineNumbers +clientBuilder.setOption(ClientConfigProperties.httpHeader("X-ClickHouse-Quota"), "test"); +// equivalent to: +clientBuilder.setOption("http_header_X-CLICKHOUSE-QUOTA", "test"); +``` + +## ClickHouse Cloud Features {#clickhouse-cloud-features} + +Features that apply when connecting to [ClickHouse Cloud](https://clickhouse.com/cloud). + +### Sticky sessions {#sticky-sessions} + +ClickHouse Cloud can pin related HTTP requests to the same replica. Send the `X-ClickHouse-Replica-Tag` header so a Cloud proxy routes every request that shares the tag to one replica. Use this for session-scoped objects such as temporary tables, replica-local cache reuse, or read-after-write consistency. + +**What to set:** HTTP header `X-ClickHouse-Replica-Tag` (`com.clickhouse.client.api.http.ClickHouseHttpProto.HEADER_REPLICA_TAG`). Use the same tag value for all requests that should stay on one replica. +```java showLineNumbers +String replicaTag = "my-app-session-1"; + +try (Client client = new Client.Builder() + .addEndpoint("https://your-service.clickhouse.cloud:8443") + .setUsername(user) + .setPassword(password) + .httpHeader(ClickHouseHttpProto.HEADER_REPLICA_TAG, replicaTag) + .build()) { + client.query("SELECT hostname()").get(); +} ``` -When options are set via `setOption` method (either the `Client.Builder` or operation settings class) then custom header name should be prefixed with `http_header_`. Method `com.clickhouse.client.api.ClientConfigProperties#httpHeader()` may be handy in this case. +To configure per operation, set the header on `QuerySettings`, `InsertSettings`, or `CommandSettings` instead of the client builder. ## Common Definitions {#common-definitions} @@ -524,6 +585,9 @@ Configuration options for insert operations. | `setInputStreamCopyBufferSize(int size)` | Copy buffer size. The buffer is used during write operations to copy data from user-provided input stream to an output stream. Default: `8196`. | | `serverSetting(String name, String value)` | Sets individual server settings for an operation. | | `serverSetting(String name, Collection values)` | Sets individual server settings with multiple values for an operation. Items of the collection should be `String` values. | +| `httpHeader(String key, String value)` | Sets a custom HTTP header for the insert. | +| `httpHeader(String key, Collection values)` | Sets a custom HTTP header with multiple values. | +| `httpHeaders(Map headers)` | Sets multiple custom HTTP headers. | | `setDBRoles(Collection dbRoles)` | Sets DB roles to be set before executing an operation. Items of the collection should be `String` values. | | `setOption(String option, Object value)` | Sets a configuration option in raw format. This isn't a server setting. | @@ -691,6 +755,9 @@ Configuration options for query operations. | `setUseTimeZone(String timeZone)` | Requests server to use `timeZone` for time conversion. See [session_timezone](/reference/settings/session-settings#session_timezone). | | `serverSetting(String name, String value)` | Sets individual server settings for an operation. | | `serverSetting(String name, Collection values)` | Sets individual server settings with multiple values for an operation. Items of the collection should be `String` values. | +| `httpHeader(String key, String value)` | Sets a custom HTTP header for the query. | +| `httpHeader(String key, Collection values)` | Sets a custom HTTP header with multiple values. | +| `httpHeaders(Map headers)` | Sets multiple custom HTTP headers. | | `setDBRoles(Collection dbRoles)` | Sets DB roles to be set before executing an operation. Items of the collection should be `String` values. | | `setOption(String option, Object value)` | Sets a configuration option in raw format. This isn't a server setting. | @@ -1482,21 +1549,49 @@ Java client provides configuration options to set up failover and retry behavior | retry | `0` | Maximum number of times retry can happen for a request. Zero or a negative value means no retry. Retry sends a request to the same node and only if the ClickHouse server returns the `NETWORK_ERROR` error code | | repeat_on_session_lock | `true` | Whether to repeat execution when the session is locked until timed out(according to `session_timeout` or `connect_timeout`). The failed request is repeated if the ClickHouse server returns the `SESSION_IS_LOCKED` error code | -### Adding custom http headers {#v1-adding-custom-http-headers} +### Adding custom HTTP headers {#v1-adding-custom-http-headers} -Java client support HTTP/S transport layer in case we want to add custom HTTP headers to the request. -We should use the custom_http_headers property, and the headers need to be `,` separated. The header key/value should be divided using `=` +The HTTP transport accepts extra request headers through the `custom_http_headers` property (`ClickHouseHttpOption.CUSTOM_HEADERS`). Each header is `Name=Value`. Separate multiple headers with a comma. Escape a comma inside a value as `\,`. An `=` inside a value is kept as part of the value. -## Java Client support {#v1-java-client-support} +**Single header** + +```java +options.put("custom_http_headers", "X-ClickHouse-Quota=test"); +// or +options.put(ClickHouseHttpOption.CUSTOM_HEADERS, "X-ClickHouse-Quota=test"); +``` + +**Multiple headers** ```java options.put("custom_http_headers", "X-ClickHouse-Quota=test, X-ClickHouse-Test=test"); ``` -## JDBC Driver {#v1-jdbc-driver} +You can also set headers on a request: + +```java +client.read(server) + .option(ClickHouseHttpOption.CUSTOM_HEADERS, "X-ClickHouse-Quota=test, X-ClickHouse-Test=test") + .query("SELECT 1") + .executeAndWait(); +``` + +## ClickHouse Cloud Features {#v1-clickhouse-cloud-features} + +Features that apply when connecting to [ClickHouse Cloud](https://clickhouse.com/cloud). + +### Sticky sessions {#v1-sticky-sessions} + +ClickHouse Cloud can pin related HTTP requests to the same replica. Send the `X-ClickHouse-Replica-Tag` header so a Cloud proxy routes every request that shares the tag to one replica. Use this for session-scoped objects such as temporary tables, replica-local cache reuse, or read-after-write consistency. + +**What to set:** HTTP header `X-ClickHouse-Replica-Tag` (`com.clickhouse.client.http.config.ClickHouseHttpOption.HEADER_REPLICA_TAG`) via `custom_http_headers`. Use the same tag value for all requests that should stay on one replica. ```java -properties.setProperty("custom_http_headers", "X-ClickHouse-Quota=test, X-ClickHouse-Test=test"); +String replicaTag = "my-app-session-1"; +options.put("custom_http_headers", "X-ClickHouse-Replica-Tag=" + replicaTag); +// or +options.put(ClickHouseHttpOption.CUSTOM_HEADERS, + ClickHouseHttpOption.HEADER_REPLICA_TAG + "=" + replicaTag); ``` diff --git a/docs/clickhouse-docs/jdbc.mdx b/docs/clickhouse-docs/jdbc.mdx index 66f44e53e..3e6ad5130 100644 --- a/docs/clickhouse-docs/jdbc.mdx +++ b/docs/clickhouse-docs/jdbc.mdx @@ -186,6 +186,73 @@ stmt.getLocalSettings().logComment("some-comment"); **Note:** this approach work for single threaded uses of statement because `localSettings` is shared between threads. +### Custom HTTP Headers {#custom-http-headers} + +Custom HTTP headers are passed as client properties. Each header uses the `http_header_` prefix. `ClientConfigProperties.httpHeader(String)` (or `DriverProperties.httpHeader(String)`) builds that key. The V1 `custom_http_headers` property is not used in JDBC V2. + +**Single header** + +```java showLineNumbers +Properties properties = new Properties(); +properties.setProperty(ClientConfigProperties.httpHeader("X-ClickHouse-Quota"), "test"); +// equivalent to: +properties.setProperty("http_header_X-CLICKHOUSE-QUOTA", "test"); + +Connection conn = Driver.connect("jdbc:ch:https://localhost:8443/", properties); +``` + +**Multiple headers** + +Set one property per header: + +```java showLineNumbers +Properties properties = new Properties(); +properties.setProperty(ClientConfigProperties.httpHeader("X-ClickHouse-Quota"), "test"); +properties.setProperty(ClientConfigProperties.httpHeader("X-ClickHouse-Test"), "test"); +``` + +The same keys can be added to the JDBC URL: + +``` +jdbc:ch:https://localhost:8443?http_header_X-CLICKHOUSE-QUOTA=test&http_header_X-CLICKHOUSE-TEST=test +``` + +To set a header for one statement only, use `StatementImpl#getLocalSettings`: + +```java showLineNumbers +StatementImpl stmt = (StatementImpl) conn.createStatement(); +stmt.getLocalSettings().httpHeader("X-ClickHouse-Quota", "test"); +``` + +## ClickHouse Cloud Features {#clickhouse-cloud-features} + +Features that apply when connecting to [ClickHouse Cloud](https://clickhouse.com/cloud). + +### Sticky sessions {#sticky-sessions} + +ClickHouse Cloud can pin related HTTP requests to the same replica. Send the `X-ClickHouse-Replica-Tag` header so a Cloud proxy routes every request that shares the tag to one replica. Use this for session-scoped objects such as temporary tables, replica-local cache reuse, or read-after-write consistency. + +**What to set:** HTTP header `X-ClickHouse-Replica-Tag` (`com.clickhouse.client.api.http.ClickHouseHttpProto.HEADER_REPLICA_TAG`). Use the same tag value for all requests that should stay on one replica. + +```java showLineNumbers +String replicaTag = "my-app-session-1"; + +Properties properties = new Properties(); +properties.setProperty("user", user); +properties.setProperty("password", password); +properties.setProperty( + ClientConfigProperties.httpHeader(ClickHouseHttpProto.HEADER_REPLICA_TAG), + replicaTag); + +Connection conn = Driver.connect("jdbc:ch:https://your-service.clickhouse.cloud:8443", properties); +``` + +URL equivalent: + +``` +jdbc:ch:https://your-service.clickhouse.cloud:8443?http_header_X-CLICKHOUSE-REPLICA-TAG=my-app-session-1 +``` + ## Supported data types {#supported-data-types} JDBC driver supports the same data formats as the underlying [java client](/integrations/language-clients/java/index#supported-data-types). @@ -762,6 +829,7 @@ properties.setProperty("socket_keepalive", "true"); - Streaming Data isn't supported in JDBC V2 because it isn't part of the JDBC spec and Java. - JDBC V2 expects explicit configuration. No failover defaults. - Protocol should be specified in the URL. No implicit protocol detection using port numbers. +- Custom HTTP headers use one property per header (`http_header_*`). The V1 comma-separated `custom_http_headers` property is not accepted. ### Configuration Changes {#configuration-changes} @@ -1020,6 +1088,51 @@ Since version `0.5.0`, we're using Apache HTTP Client that's packed the Client. Note: please refer to [JDBC specific configuration](https://github.com/ClickHouse/clickhouse-java/blob/main/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcConfig.java) for more. +### Custom HTTP Headers {#v07-custom-http-headers} + +Extra request headers are set with `custom_http_headers`. Each header is `Name=Value`. Separate multiple headers with a comma. Escape a comma inside a value as `\,`. An `=` inside a value is kept as part of the value. + +**Single header** + +```java +properties.setProperty("custom_http_headers", "X-ClickHouse-Quota=test"); +``` + +**Multiple headers** + +```java +properties.setProperty("custom_http_headers", "X-ClickHouse-Quota=test, X-ClickHouse-Test=test"); +``` + +The same property can be added to the JDBC URL. URL-encode `=` in the value as `%3D`: + +``` +jdbc:ch:http://localhost:8123?custom_http_headers=X-ClickHouse-Quota%3Dtest,X-ClickHouse-Test%3Dtest +``` + +## ClickHouse Cloud Features {#v07-clickhouse-cloud-features} + +Features that apply when connecting to [ClickHouse Cloud](https://clickhouse.com/cloud). + +### Sticky sessions {#v07-sticky-sessions} + +ClickHouse Cloud can pin related HTTP requests to the same replica. Send the `X-ClickHouse-Replica-Tag` header so a Cloud proxy routes every request that shares the tag to one replica. Use this for session-scoped objects such as temporary tables, replica-local cache reuse, or read-after-write consistency. + +**What to set:** HTTP header `X-ClickHouse-Replica-Tag` (`com.clickhouse.client.http.config.ClickHouseHttpOption.HEADER_REPLICA_TAG`) via `custom_http_headers`. Use the same tag value for all requests that should stay on one replica. + +```java +String replicaTag = "my-app-session-1"; + +Properties properties = new Properties(); +properties.setProperty("custom_http_headers", "X-ClickHouse-Replica-Tag=" + replicaTag); + +ClickHouseDataSource dataSource = new ClickHouseDataSource( + "jdbc:clickhouse:https://your-service.clickhouse.cloud:8443", properties); +try (Connection conn = dataSource.getConnection("default", "password")) { + // requests on this connection are pinned to the replica selected for replicaTag +} +``` + ## Supported data types {#v07-supported-data-types} JDBC driver supports same data formats as client library does.