Repository navigation
[jdbc-v2] setClientInfo(ApplicationName) removes the detected frameworks from the User-Agent #3219
Description
Description
When jdbc-v2 detects a framework on the call stack (apache.spark, apache.flink, apache.nifi), it adds a framework group to the User-Agent header. If the application then calls Connection.setClientInfo("ApplicationName", ...), the framework group is removed for all later requests on that connection.
If you set the same application name at connect time (client_name), the group stays. So the result depends on how the application name is set:
client_name=my-app/1.0 at connect: my-app/1.0 jdbc-v2/0.12.0-rc1-SNAPSHOT ((apache.spark)) clickhouse-java-v2/0.12.0-rc1-SNAPSHOT (Linux; jvm:17.0.20.1) Apache-HttpClient/5.6.4
setClientInfo("ApplicationName", "my-app/1.0"): my-app/1.0 jdbc-v2/0.12.0-rc1-SNAPSHOT clickhouse-java-v2/0.12.0-rc1-SNAPSHOT (Linux; jvm:17.0.20.1) Apache-HttpClient/5.6.4
The docs say that the driver always appends the detected frameworks. docs/integration-jdbc.md, "How it is observed on the server": "The driver automatically appends the JDBC driver version, detected frameworks (e.g. HikariCP), client version, operating system, and JVM version". setClientInfo("ApplicationName", ...) is documented in the same section as the way to set the application name on a pooled connection.
Root cause
The connection builds the client name in two places, and only the first one adds the frameworks:
- The
ConnectionImplconstructor builds[<app> ]jdbc-v2/<v>and then appends" (" + detectedFrameworks + ")":
clickhouse-java/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java
Lines 89 to 107 in 5392b64
String clientName = Driver.DRIVER_CLIENT_NAME + Driver.getLibraryVersion(); Map<String, String> clientProperties = config.getClientProperties(); if (clientProperties.get(ClientConfigProperties.CLIENT_NAME.getKey()) != null) { this.appName = clientProperties.get(ClientConfigProperties.CLIENT_NAME.getKey()).trim(); clientName = this.appName + " " + clientName; // Use the application name as client name } else if (clientProperties.get(ClientConfigProperties.PRODUCT_NAME.getKey()) != null) { // Backward compatibility for old property this.appName = clientProperties.get(ClientConfigProperties.PRODUCT_NAME.getKey()).trim(); clientName = this.appName + " " + clientName; // Use the application name as client name } if (!this.config.isDisableFrameworkDetection()) { String detectedFrameworks = Driver.FrameworksDetection.getFrameworksDetected(); LOG.debug("Detected frameworks: {}", detectedFrameworks); if (!detectedFrameworks.trim().isEmpty()) { clientName += " (" + detectedFrameworks + ")"; } } ConnectionImpl.setClientInfocallsJdbcConfiguration.updateUserClient(value, client):
clickhouse-java/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java
Lines 576 to 582 in 5392b64
public void setClientInfo(String name, String value) throws SQLClientInfoException { if (ClientInfoProperties.APPLICATION_NAME.getKey().equals(name)) { config.updateUserClient(value, client); appName = value; } // TODO: generate warning for unknown properties }
updateUserClientreplaces the whole client name with<value> + ' ' + getDefaultClientName().getDefaultClientName()is onlyjdbc-v2/<v>, so the framework group is lost:
clickhouse-java/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java
Lines 478 to 488 in 5392b64
public void updateUserClient(String clientName, Client client) { client.updateClientName((clientName == null || clientName.isEmpty() ? "" : clientName) + ' ' + getDefaultClientName()); } public static String getDefaultClientName() { StringBuilder jdbcName = new StringBuilder(); jdbcName.append(Driver.DRIVER_CLIENT_NAME) .append(Driver.getLibraryVersion()); return jdbcName.toString(); }
All values of setClientInfo go through this path: a name, null, "", and setClientInfo(Properties). setClientInfo(Properties) resets ApplicationName to null when the key is not in the properties.
Suggested fix
Build the client name in one place and use it in both the constructor and setClientInfo: [<app> ]jdbc-v2/<v>[ <framework group>]. Add the framework group only when framework detection is enabled for the connection. Driver.FrameworksDetection.getFrameworksDetected() caches its result, so calling it again from setClientInfo is cheap.
These cases must not change:
- No framework on the stack: no group.
disable_frameworks_detection=true: no group, also aftersetClientInfo. (Today this property fails the connection, see [jdbc-v2] Documented driver property disable_frameworks_detection makes the connection fail: Unknown and unmapped config properties #3218.)setClientInfo("ApplicationName", null)or""removes only the application name. The rest of the name stays.
With a null or empty value, updateUserClient makes a client name with a leading space (" jdbc-v2/<v>"). The server does not show it, because HTTP header parsers remove leading whitespace from a field value. The shared helper should not make the leading space.
A regression test can extend ConnectionTest#setAndGetClientInfoTest (it already reads http_user_agent from system.query_log). The test must call the connection from a class in an org.apache.spark.* package, and reset the cached Driver.FrameworksDetection.frameworksDetected (package-private) first. Then it can check that the framework group is the same before and after setClientInfo.
Related:
- [jdbc-v2] User-Agent wraps detected frameworks in double parentheses: ((apache.spark)) #3214 (open) is about the format of the framework group (
((apache.spark))). This issue is about the group being removed. A fix that builds the name in one place can fix both. - [client-v2] Send the User-Agent in the standard client format and add client metadata #3213 (open, for [client-v2] Build the User-Agent in the standard client format and let applications add metadata #3202) changes the client-v2 part of the header. It keeps
client_metadatawhen the client name changes, but it does not change how jdbc-v2 builds the client name. This bug is still present with it.
Steps to reproduce
- Put a class in a package whose name contains
apache.spark(for exampleorg.apache.spark.repro). Open a jdbc-v2 connection from it, withoutclient_name. - Call
conn.setClientInfo("ApplicationName", "my-app/1.0"). - Run a query that has a unique marker, then
SYSTEM FLUSH LOGS, then readhttp_user_agentfor that query fromsystem.query_log.
Error Log or Exception StackTrace
No exception. http_user_agent before and after the call on the same connection. Each row is a separate JVM, because the detection result is cached per JVM:
[spark on stack] no setClientInfo jdbc-v2/0.12.0-rc1-SNAPSHOT ((apache.spark)) clickhouse-java-v2/... (same after)
[spark on stack] client_name=my-app/1.0, no call my-app/1.0 jdbc-v2/0.12.0-rc1-SNAPSHOT ((apache.spark)) clickhouse-java-v2/... (same after)
[spark on stack] setClientInfo(AppName,"my-app/1.0") before: jdbc-v2/0.12.0-rc1-SNAPSHOT ((apache.spark)) clickhouse-java-v2/...
after: my-app/1.0 jdbc-v2/0.12.0-rc1-SNAPSHOT clickhouse-java-v2/...
[spark on stack] setClientInfo(AppName, null) after: jdbc-v2/0.12.0-rc1-SNAPSHOT clickhouse-java-v2/...
[spark on stack] setClientInfo(AppName, "") after: jdbc-v2/0.12.0-rc1-SNAPSHOT clickhouse-java-v2/...
[spark on stack] client_name=my-app/1.0, then
setClientInfo(new Properties()) after: jdbc-v2/0.12.0-rc1-SNAPSHOT clickhouse-java-v2/...
[spark on stack] client_name=my-app/1.0, then
setClientInfo(AppName,"other-app/2.0") after: other-app/2.0 jdbc-v2/0.12.0-rc1-SNAPSHOT clickhouse-java-v2/...
[no framework] setClientInfo(AppName,"my-app/1.0") after: my-app/1.0 jdbc-v2/0.12.0-rc1-SNAPSHOT clickhouse-java-v2/... (correct, no group expected)
(clickhouse-java-v2/... is clickhouse-java-v2/0.12.0-rc1-SNAPSHOT (Linux; jvm:17.0.20.1) Apache-HttpClient/5.6.4 in all rows.)
Expected Behaviour
setClientInfo("ApplicationName", ...) changes only the application name. The framework group stays, the same as when the name is set with client_name:
my-app/1.0 jdbc-v2/0.12.0-rc1-SNAPSHOT ((apache.spark)) clickhouse-java-v2/0.12.0-rc1-SNAPSHOT ...
(The format of the group is #3214.)
Code Example
package org.apache.spark.repro; // "apache.spark" in the stack trace triggers detection
import java.sql.*;
import java.util.*;
public class SparkApp {
public static void main(String[] args) throws Exception {
Properties p = new Properties();
p.setProperty("user", "default");
p.setProperty("password", "<password>");
try (Connection c = new com.clickhouse.jdbc.Driver().connect("jdbc:clickhouse://localhost:8123/default", p)) {
System.out.println(userAgent(c)); // jdbc-v2/<v> ((apache.spark)) clickhouse-java-v2/<v> (...)
c.setClientInfo("ApplicationName", "my-app/1.0");
System.out.println(userAgent(c)); // my-app/1.0 jdbc-v2/<v> clickhouse-java-v2/<v> (...) <- group is gone
}
}
static String userAgent(Connection c) throws SQLException {
String marker = "ua_probe_" + UUID.randomUUID().toString().replace("-", "");
try (Statement s = c.createStatement()) {
s.executeQuery("SELECT '" + marker + "'").close();
s.execute("SYSTEM FLUSH LOGS");
try (ResultSet rs = s.executeQuery("SELECT http_user_agent FROM system.query_log"
+ " WHERE type = 'QueryFinish' AND query LIKE '%" + marker + "%'"
+ " AND query NOT LIKE '%query_log%' LIMIT 1")) {
rs.next();
return rs.getString(1);
}
}
}
}Configuration
Client Configuration
Properties: user, password. No client_name (for the setClientInfo case).Environment
- Cloud
- Client version: jdbc-v2 0.12.0-rc1-SNAPSHOT, built from
mainat 5392b64 - Language version: Java 17.0.20.1
- OS: Linux
ClickHouse Server
- ClickHouse Server version: 26.9.1.1629
- ClickHouse Server non-default settings, if any: none relevant
CREATE TABLEstatements for tables involved: none- Sample data for all these tables: none
Found by automated analysis of the client (Polyglot AI) while investigating #3214. Verified against a live server, not only by reading the code.
Activity