Repository navigation
[client-v2] Send the User-Agent in the standard client format and add client metadata - #3213
Conversation
… client metadata The User-Agent header is now '<client name> clickhouse-java-v2/<version> (<key>:<value>; ...; lv:java/<java.version>; os:<os.name>)', the format that clickhouse-go, clickhouse-js, clickhouse-connect and clickhouse-rs use. 'jvm:' becomes 'lv:java/', the OS becomes the 'os:' entry, and the trailing 'Apache-HttpClient/<version>' token is removed. Client.Builder#setClientMetadata(Map) (property 'client_metadata', 'key1=value1,key2=value2') adds key/value pairs to the comment group, before 'lv' and 'os'. Entries that would break the format are rejected with ClientMisconfigurationException when the client is built. Implements: #3202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client V2 CoverageCoverage Report
Class Coverage
|
JDBC V2 CoverageCoverage Report
Class Coverage
|
JDBC V1 CoverageCoverage Report
Class Coverage
|
Client V1 CoverageCoverage Report
Class Coverage
|
chernser
left a comment
There was a problem hiding this comment.
- move client identification logic into WorkloadIdentitySupport class.
- make it around
User-Agentheader so future transport implementations do not skip this feature - add another method com.clickhouse.client.api.Client.Builder#setClientName with metadata options
- User still should specify set client name instead of setting User-Agent header - this should be stated in the documentation.
Address review feedback on #3213: - Add WorkloadIdentitySupport. It builds the User-Agent value (client name or custom User-Agent, then clickhouse-java-v2/<version> with the metadata, lv and os entries) and validates client metadata entries. HttpAPIClientHelper only reads a custom User-Agent header and sets the value that the class returns. - Replace Client.Builder#setClientMetadata(Map) with Client.Builder#setClientName(String, Map). setClientName(String) does not change the metadata. The client_metadata property is unchanged. - State in the docs and in the httpHeader Javadoc that applications set the client name, not the User-Agent header. Implements: #3202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@chernser Thank you for the review. I pushed aabc06e:
Not changed: jdbc-v2 still builds its own application part ( Tests (ClickHouse 26.9): client-v2 unit 760/760, jdbc-v2 unit 1963/1963, client-v2 User-Agent integration tests 11/11, jdbc-v2 User-Agent integration tests 6/6, |
chernser
left a comment
There was a problem hiding this comment.
Good - logic is in separate file. But implementation is far from optimal
- cache metadata. it is a massive string and should be validated upon build and composed only once.
- mandatory user agent part must be composed once and cached.
- ClientConfigurationProperties cannot hold default value because it will be rebuilt everytime user create client with custom user agent or client name (see previous logic)
- The mandatory part (clickhouse-java-v2/<version>, lv, os) is a static constant, composed once. - The client metadata is validated when the client is built and composed once per client. - The complete value is cached and composed again only when the application part changes (client name or custom User-Agent). - The application part is trimmed; use ClientUtils#isBlank. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@chernser Thank you for the review. I pushed 3abea18 for your review of 2026-10-19T19:28Z:
I also removed the Tests
Results (CH 26.9)
|
SonarCloud rule java:S3077 flags the volatile reference to the cached User-Agent. Use an AtomicReference, as CredentialsManager does. The behavior does not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TriageCategory: Summary What this impacts
Concerns
Required reviewer action
|
chernser
left a comment
There was a problem hiding this comment.
CLIENT_METADATA should not have default value
there should be no constants for parts like this - it is hard to maintain because logic get disconnected.
Make just one branch - compose client part once.
compose client can be a bit heavy in logic - it is called once. But it is important to keep this logic in one place and do not have many branches.
Review follow-up for #3213: - Remove the static constants for parts of the User-Agent (start, end, value without metadata). composeClientPart now composes the complete client part with one code path, once per client, when the client is created. This replaces the per-JVM constants from 3abea18. - Read client_metadata only if it is set (applyIfSet), with no default. - Replace 0x20 / 0x7E with isPrintableAscii (' ' to '~'), and explain the forbidden characters of a metadata entry. - Tests: replace testMandatoryPartSharedByClients (it pinned the shared static part) with a test for no / empty metadata that also catches state shared between instances; add rows for the ends of the printable ASCII range (0x1F, '~', 0x7F). Implements: #3202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@chernser Thank you. I pushed 8fa4ebd for your review of 2026-10-19T21:02Z:
What I kept, and why:
Tests
Results (CH 26.9)
|
…name client_id_metadata - HttpAPIClientHelper keeps one final WorkloadIdentitySupport (no AtomicReference). Client#updateClientName calls WorkloadIdentitySupport#updateClientName, which composes the value again and keeps it (synchronized; volatile strings). - A blank application part for one operation gets the kept value. - Client metadata can set lv, os and ahc (keys compared ignoring case); the client adds its own entry only for a key that is not set. - New ahc entry: the Apache HttpClient version, from the generated class DependencyVersions (templating-maven-plugin), so the version is correct also in the shaded jar. - Property client_metadata renamed to client_id_metadata. - compose -> composeUserAgent; START_OF_ASCII_RANGE / END_OF_ASCII_RANGE constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@chernser Thank you. I pushed 014290c for your review of 2026-10-20T01:01Z and the 8 inline comments (I replied in each thread):
Tests (CH 26.9): client-v2 unit 779/779, jdbc-v2 unit 1963/1963, client-v2 User-Agent ITs 11/11, jdbc-v2 Two questions are in the |
…>, add Versions - Client creates the WorkloadIdentitySupport, keeps it in a final field and updates it in updateClientName. HttpAPIClientHelper gets it in its constructor and only sets the User-Agent header from it; HttpAPIClientHelper#updateClientName is removed. - The HTTP client entry is now hc:ahc_<Apache HttpClient version>. - The generated class DependencyVersions is now Versions. It also has the client version (CLIENT), so client-v2-version.properties is not read any more and is removed, with the resource filtering block. Client.clientVersion comes from Versions.CLIENT and is not a compile-time constant. - addIfNotSet moved to ClientUtils#putIfAbsentIgnoreCase (null key is rejected). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@chernser Thank you. I pushed a545bcf for your review of 2026-10-20T01:45Z and the 4 inline comments (I replied in each thread):
Results (ClickHouse 26.9): client-v2 unit 786/786, client-v2 User-Agent integration tests 11/11, jdbc-v2 unit 1963/1963, jdbc-v2 |
chernser
left a comment
There was a problem hiding this comment.
see comments. coverage still 88%
…oc examples as methods - WorkloadIdentitySupportTest: one data-provider test for the value of a request and one for client name updates, plus a test that two instances do not share the client part. - UserAgentUnitTest: entries with a null key or a null value in the setClientName(String, Map) metadata are ignored. - Client.Builder#setClientName(String, Map) Javadoc and features.md: a null key or value is ignored. - docs: wrap the client metadata examples as methods. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@chernser Thank you. I pushed 3d8771a for your review of 2026-10-20T05:27Z (I replied in the 3 threads):
|
|
…er-agent [v0.11.0] Send the User-Agent in the standard client format and add client metadata (backport #3213)



Summary
Implements #3202. client-v2 now sends the
User-Agentheader in the standard ClickHouse client format, which clickhouse-go, clickhouse-js, clickhouse-connect and clickhouse-rs already use:jvm:<version>is nowlv:java/<version>. The OS is now theos:entry.Apache-HttpClient/<version>token is now thehc:ahc_<version>entry:hcis the HTTP client,ahc_is Apache HttpClient (maintainer review). The version comes from the generated classVersions, so it is correct also in the shaded jar.Client.Builder#setClientName(String, Map<String, String>)sets the client name and adds key/value pairs to the comment group. The client addslv,osandhconly for keys that the metadata does not set.WorkloadIdentitySupport. It builds the completeUser-Agentvalue, so a transport only sets the header.Example from the issue:
Closes #3202
Design
WorkloadIdentitySupport(client-v2/.../api/internal) owns the client identification.getUserAgent(customUserAgent, requestConfig)returns the completeUser-Agentvalue: the application part (a customUser-Agentheader if one is set, else the client name; trimmed; blank counts as not set), thenclickhouse-java-v2/<version> (<metadata>; lv:...; os:...; hc:ahc_...). It also validates metadata entries.HttpAPIClientHelperonly reads the custom header and sets the returned value, so a new transport gets the same identification from the same class.Config property
client_id_metadata(ClientConfigProperties.CLIENT_ID_METADATA,Map.class, appended at the end of the enum). It follows the existingtype_hint_mappingpattern: string formkey1=value1,key2=value2, parsed bytoKeyValuePairsinto an ordered map. Because it is a property, JDBC gets it through connection properties and URL parameters without jdbc-v2 code changes.Builder method
setClientName(String, Map)(an overload ofsetClientName) writes the client name and that string form.setClientName(String)does not change the metadata (jdbc-v2 calls it after it applies the connection properties). Escaping is handled by the new package-privateClientConfigProperties.fromKeyValuePairs(inverse oftoKeyValuePairs). Entries keep the iteration order of the map.nullor an empty map removes the setting.Caching. The metadata is validated when the client is built. One method,
composeClientPart, composes the complete client part (clickhouse-java-v2/<version> (<metadata>; lv:java/...; os:...; hc:ahc_...)) one time per client, with one code path and no static part constants. The metadata entries come first;lv,osandhcare added only if the metadata does not set them (keys compared ignoring case, withClientUtils#putIfAbsentIgnoreCase).CLIENT_ID_METADATAhas no default value; it is read only if set.Clientcreates oneWorkloadIdentitySupport, keeps it in a final field, and gives it to theHttpAPIClientHelperconstructor. The helper only sets the header from it for each request. The constructor composes the value for the client configuration (application part = the client-levelUser-Agentheader if set, else the client name) and keeps it.Client#updateClientNamecallsWorkloadIdentitySupport#updateClientNameon its instance directly, which composes the value again and keeps it (synchronized; the kept strings arevolatile). A request with another application part (aclient_name/User-Agentset for one operation) gets a new string that reuses the client part, and the kept value does not change. A blank value set for one operation counts as not set and gets the kept value.ClientConfigPropertiesholds only the raw inputs, with no default or composed value.Long value warning. The client logs a warning (no exception) when the kept
User-Agentvalue is longer than 200 characters, when the client is created and when the client name changes. A value composed for one request is not logged (docs/changes_checklist.md: no logs in the request path).Validation is called from
CLIENT_ID_METADATA.parseValue(rules inWorkloadIdentitySupport.checkClientMetadataEntry), so it covers the builder,setOptionand JDBC. It is the same pattern asssl_cipher_suites.build()throwsClientMisconfigurationExceptionif::;(),=;;().These rules keep the comment group parseable. Keys and values are trimmed, and entries with a
nullor empty key or value are ignored.Client-level only. The metadata is put into the client part when the client is created. It is kept when
Client#updateClientName/ JDBCsetClientInfo(ApplicationName)changes the application part, and when a customUser-Agentheader is set. It is not read from operation settings. This is documented, and a test covers it.The application part is unchanged: a custom
User-Agentheader, else the client name. The docs and thehttpHeaderJavadoc state that applications must set the client name, not theUser-Agentheader.Compatibility
This intentionally changes the HTTP
User-Agentoutput (a high-risk surface perdocs/ai-review.md). The change is requested in #3202.system.query_log.http_user_agentforjvm:orApache-HttpClient/must use the new format.internalpackage is not public API). Added: one enum constant (at the end), one builder overloadsetClientName(String, Map), the internal classWorkloadIdentitySupport, the internal methodClientUtils#putIfAbsentIgnoreCase, and the generated internal classVersions. The internalHttpAPIClientHelperconstructor gets aWorkloadIdentitySupportparameter.templating-maven-plugin(version managed in the root pom) to generateVersionsfromsrc/main/java-templates.apache.http.client.versionkey fromclient-v2-version.properties.Versions.APACHE_HTTP_CLIENTreplaces it.ahc_<version>is the version that the client is built with; before, the non-shaded jar reported the runtime package version.Client.clientVersionis nowVersions.CLIENT(${revision}, the same value as theversionkey ofclient-v2-version.properties). Nothing readsclient-v2-version.propertiesany more, so the file and the client-v2 resource filtering block are removed.Client.clientVersionis set withString.valueOf, so it is not a compile-time constant: code compiled against it reads the version of the client that it runs with, as before.Not changed: jdbc-v2 still adds detected frameworks as its own group after
jdbc-v2/<version>(e.g.((apache.spark)), with double parentheses). That is a separate issue. Moving frameworks intoclient_id_metadatawould change jdbc-v2 output and needs a key name, so it is left for a follow-up.Changes
WorkloadIdentitySupport(new, internal): builds theUser-Agentvalue in the standard format plus metadata, and validates metadata entries.ClientConfigProperties:CLIENT_ID_METADATA;fromKeyValuePairshelper.Client: creates theWorkloadIdentitySupportand updates it inupdateClientName;clientVersioncomes fromVersions.CLIENT.Client.Builder#setClientName(String, Map);setClientName(String)andhttpHeaderJavadoc state to use the client name, not theUser-Agentheader. Same note onCommonSettings#httpHeader.HttpAPIClientHelper: gets the client'sWorkloadIdentitySupportin its constructor and sets theUser-Agentvalue that it returns. Removed the Apache HttpClient version lookup.Versions(generated, internal) +templating-maven-plugininclient-v2/pom.xml(version in the root pom): the client version and the Apache HttpClient version.ClientUtils#putIfAbsentIgnoreCase(new, internal).docs/features.md(client-v2 entry, jdbc-v2 client-info bullet),docs/clickhouse-docs/client.mdx(builder table, Client Identification),docs/clickhouse-docs/jdbc.mdx,docs/integration-client.md,docs/integration-jdbc.md.history/latest/3202.md.Test
UserAgentUnitTest(new, WireMock): asserts the exact header on the wire throughClient#query. Rows: no name; the issue example;setOptionstring form; escaped,/=/\in a value; entries with an empty ornullkey or value ignored; empty and blank client name with metadata;setOptionmetadata kept bysetClientName(String)and replaced bysetClientName(String, Map);setClientName(String)keeps metadata; metadata cleared withnulland with an empty map; customUser-Agentheader plus metadata. Also: metadata that replacesos. A second test coversupdateClientName, an operation-levelUser-Agentheader, and operation-levelclient_id_metadata(ignored). A third test checks that a blank operation-level client name gets the client value. A fourth test checks that invalid metadata is rejected atbuild().WorkloadIdentitySupportTest(new, data providers): one row for each request case (client name, metadata, customUser-Agent, operation-level client name, blank values, trimming) — a value equal to the client value must be the kept instance (assertSame); one row for eachupdateClientNamecase; metadata replaceslv/os/hc(also upper-case keys); two instances do not share the client part;VersionsTestchecks thatVersions.CLIENTis a resolved version and thatVersions.APACHE_HTTP_CLIENTequals the version that httpclient5 reports (VersionInfo).ClientUtilsTest#testPutIfAbsentIgnoreCase(new): a new key is added (also to an empty map); the same key, in the same or in a different case, and also when it is not the first key, does not change the map; anullkey is rejected.HttpAPIClientHelperTest: through a realClient,updateClientNamecomposes the value again and keeps it (assertSame), and a value longer than 200 characters is logged at client creation and onupdateClientName, not per request.ClientConfigPropertiesTest: valid parse rows (order, trim, empty entries,:and=in values). Invalid rows: each forbidden character, control and non-ASCII characters, stray,/ tokens without=.osandlvkeys are valid. Also anullrow and a round trip offromKeyValuePairs.HttpTransportTests#testUserAgentHasCompleteProductName: the data-provider patterns asserted the old format, so they now assert the new format withlv:,os:andhc:ahc_. This is the only existing test whose expectations changed.ConnectionTest#testClientMetadataInUserAgent(new, jdbc-v2 integration):client_id_metadataas a connection property and as a URL parameter, checked insystem.query_log, also aftersetClientInfo(ApplicationName).executeQueryAndVerifyUserAgentnow also returns the user agent. Its assertions did not change.os:makes 5UserAgentUnitTestrows fail.Run locally (ClickHouse 26.9, head
a545bcfd9): client-v2 unit 783/783; jdbc-v2 unit 1963/1963; client-v2HttpTransportTestsUser-Agent/client-name integration tests 11/11; jdbc-v2ConnectionTestUser-Agent/client info tests 3/3; javadoc clean;packages/clickhouse-jdbc-allbuilds, and its shadedVersionskeeps both values.docs/changes_checklist.mdclient_id_metadatais unique and has no default. Its value typeMapmatchesparseValue.ClientMisconfigurationException). It does not overlap an existing property.docs/features.mdis updated.setClientName(String, Map)is an overload ofsetClientNameand follows thetypeHintMappingbuilder pattern for the map. Nullability is documented. Behavior tests are added.system.query_logfor client-v2 and jdbc-v2).warnwhen the keptUser-Agentvalue is longer than 200 characters, only when the client is created and onupdateClientName(not in the request path). The removed code had onewarn/ onedebugfor the HTTP client version lookup.HttpAPIClientHelper(newWorkloadIdentitySupportparameter). All callers are in client-v2 and are updated.Checklist
history/latest/3202.md)docs/features.md,docs/clickhouse-docs/*.mdx, integration guides)Pre-PR validation gate
system.query_log)setOption, JDBC property and URL, runtime client-name changes, custom header at client and operation levelAGENTS.md,docs/ai-review.md,docs/changes_checklist.md🤖 Generated with Claude Code
Note
Medium Risk
Intentional breaking change to every request’s
User-Agentand query-log observability; mis-parsed metadata is rejected at build time but does not affect auth or data paths.Overview
Breaking: HTTP
User-Agentnow follows the standard ClickHouse client shape (clickhouse-java-v2/<version> (lv:java/...; os:...; hc:ahc_...)), replacingjvm:and trailingApache-HttpClient/.... Anything parsingsystem.query_log.http_user_agentfor the old tokens must be updated.Adds
Client.Builder#setClientName(String, Map)and theclient_id_metadataproperty so apps can attach validated key/value pairs to the comment group (with defaults forlv,os, andhcwhen not overridden). Identification is centralized in new internalWorkloadIdentitySupport, wired fromClientthroughHttpAPIClientHelper;updateClientNamerecomposes and caches the header (with a warning above 200 characters).Build-wise,
templating-maven-plugingeneratesVersionsfromjava-templates(client and Apache HttpClient versions for shaded jars);client-v2-version.propertiesand resource filtering are removed.Client.clientVersionreadsVersions.CLIENTat runtime.Docs, changelog (
history/latest/3202.md), and jdbc-v2 inherit the new header via client-v2 (includingclient_id_metadataon connections).Reviewed by Cursor Bugbot for commit 3d8771a. Bugbot is set up for automated code reviews on this repo. Configure here.