Skip to content

Commit 4605fd4

Browse files
umair-ablyclaude
andcommitted
Make the side-declaring agent entries versionless
Mirrors ably-js#2297, matching the registry entries in ably-common#361: the side flags are registered versionless (like `browser`), because under lockstep versioning a side-flag version always duplicates the SDK entry beside it, and across SDKs a bare version cannot say what it versions. Identity, version and support status keep travelling on the SDK's own ably-java/<version> entry. Wire shape: ably-java/2.0.0 jre/17 ably-pubsub-server AgentHeaderCreator already emits a null-valued map entry as a bare token, so no core change is needed; the doors stamp null and drop their BuildConfig version plumbing. Tests now assert the flag is present as a bare token and fail if any /version form regresses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 60aabae commit 4605fd4

7 files changed

Lines changed: 67 additions & 46 deletions

File tree

‎device/build.gradle.kts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ android {
88
defaultConfig {
99
minSdk = 19
1010
compileSdk = 34
11-
buildConfigField("String", "VERSION", "\"${property("VERSION_NAME")}\"")
1211
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
1312
}
1413

@@ -23,10 +22,6 @@ android {
2322
}
2423
}
2524

26-
buildFeatures {
27-
buildConfig = true
28-
}
29-
3025
lint {
3126
abortOnError = false
3227
}

‎device/src/androidTest/java/io/ably/pubsub/device/PubSubDeviceTest.java‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
* The agent entries asserted here are what the platform reads to classify traffic on
1717
* MAU-priced accounts, so these tests are deliberately strict: if one fails, billing
1818
* classification is broken, not just a header.
19+
* <p>
20+
* The side entry is a versionless flag — a bare token on the wire, per ably/ably-common#361
21+
* — so the assertions also fail if a version (or any {@code /suffix}) reappears on it.
1922
*/
2023
public class PubSubDeviceTest {
2124

@@ -27,35 +30,43 @@ private static ClientOptions offlineOptions(String key) throws Exception {
2730
return options;
2831
}
2932

33+
/** The stamped entry is present as a versionless flag, and the other side's is absent. */
34+
private static void assertDeviceFlag(Map<String, String> agents) {
35+
assertTrue("expected the device side flag", agents.containsKey(Side.DEVICE_AGENT_IDENTIFIER));
36+
assertNull("the side flag is versionless", agents.get(Side.DEVICE_AGENT_IDENTIFIER));
37+
assertFalse("a device client must not carry the server entry",
38+
agents.containsKey(Side.SERVER_AGENT_IDENTIFIER));
39+
}
40+
3041
@Test
3142
public void client_stampsDeviceAgent() throws Exception {
3243
AblyRealtime client = PubSubDevice.clientBuilder(offlineOptions(FAKE_KEY)).build();
33-
assertEquals(BuildConfig.VERSION, client.options.agents.get(Side.DEVICE_AGENT_IDENTIFIER));
44+
assertDeviceFlag(client.options.agents);
3445
}
3546

3647
@Test
3748
public void keyString_isAcceptedAndDisambiguatedAsKey() throws Exception {
3849
ClientOptions builtOptions = PubSubDevice.clientBuilder(FAKE_KEY).build().options;
3950
assertEquals(FAKE_KEY, builtOptions.key);
4051
assertNull(builtOptions.token);
41-
assertEquals(BuildConfig.VERSION, builtOptions.agents.get(Side.DEVICE_AGENT_IDENTIFIER));
52+
assertDeviceFlag(builtOptions.agents);
4253
}
4354

4455
@Test
4556
public void callerAgentEntries_arePreserved_andCannotOverrideTheSideEntry() throws Exception {
4657
ClientOptions options = offlineOptions(FAKE_KEY);
4758
Map<String, String> callerAgents = new HashMap<>();
4859
callerAgents.put("some-sdk", "1.2.3");
49-
callerAgents.put(Side.DEVICE_AGENT_IDENTIFIER, "not-the-real-version");
60+
callerAgents.put(Side.DEVICE_AGENT_IDENTIFIER, "not-the-real-form");
5061
options.agents = callerAgents;
5162

5263
AblyRealtime client = PubSubDevice.clientBuilder(options).build();
5364
assertEquals("1.2.3", client.options.agents.get("some-sdk"));
54-
assertEquals(BuildConfig.VERSION, client.options.agents.get(Side.DEVICE_AGENT_IDENTIFIER));
65+
// The stamp replaces the caller's value: the flag is present and back to versionless.
66+
assertDeviceFlag(client.options.agents);
5567

5668
// the caller's own map is untouched
5769
assertTrue(options.agents == callerAgents);
58-
assertEquals("not-the-real-version", callerAgents.get(Side.DEVICE_AGENT_IDENTIFIER));
59-
assertFalse(callerAgents.containsValue(BuildConfig.VERSION));
70+
assertEquals("not-the-real-form", callerAgents.get(Side.DEVICE_AGENT_IDENTIFIER));
6071
}
6172
}

‎device/src/main/java/io/ably/pubsub/device/PubSubDevice.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ private ClientBuilder(ClientOptions options, String keyOrToken) {
6262
* @throws AblyException if the options, key or token are rejected.
6363
*/
6464
public AblyRealtime build() throws AblyException {
65+
// The side entry is a versionless flag — see Side.
6566
final ClientOptions stamped;
6667
if (keyOrToken != null) {
67-
stamped = Side.optionsWithSideAgent(keyOrToken, Side.DEVICE_AGENT_IDENTIFIER, BuildConfig.VERSION);
68+
stamped = Side.optionsWithSideAgent(keyOrToken, Side.DEVICE_AGENT_IDENTIFIER);
6869
} else {
69-
stamped = Side.optionsWithSideAgent(options, Side.DEVICE_AGENT_IDENTIFIER, BuildConfig.VERSION);
70+
stamped = Side.optionsWithSideAgent(options, Side.DEVICE_AGENT_IDENTIFIER);
7071
}
7172
return new AblyRealtime(stamped);
7273
}

‎server/build.gradle.kts‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
plugins {
2-
alias(libs.plugins.build.config)
32
alias(libs.plugins.maven.publish)
43
checkstyle
54
`java-library`
@@ -19,12 +18,6 @@ dependencies {
1918
testImplementation(libs.bundles.tests)
2019
}
2120

22-
buildConfig {
23-
useJavaOutput()
24-
packageName = "io.ably.pubsub.server"
25-
buildConfigField("String", "VERSION", "\"${property("VERSION_NAME")}\"")
26-
}
27-
2821
sourceSets {
2922
named("main") {
3023
java {
@@ -35,10 +28,6 @@ sourceSets {
3528
}
3629
}
3730

38-
tasks.checkstyleMain.configure {
39-
exclude("io/ably/pubsub/server/BuildConfig.java")
40-
}
41-
4231
tasks.register<Test>("runUnitTests") {
4332
beforeTest(closureOf<TestDescriptor> { logger.lifecycle("-> $this") })
4433
outputs.upToDateWhen { false }

‎server/src/main/java/io/ably/pubsub/server/PubSubServer.java‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,15 @@ public static RealtimeClientBuilder realtimeClientBuilder(String keyOrToken) {
6464

6565
/**
6666
* Resolves the caller's input exactly as the core constructors would, then stamps the
67-
* server-side agent entry. Resolution happens at {@code build()} time so the caller's
68-
* input is read once, when the client is constructed.
67+
* server-side agent entry (a versionless flag — see {@link Side}). Resolution happens
68+
* at {@code build()} time so the caller's input is read once, when the client is
69+
* constructed.
6970
*/
7071
private static ClientOptions stampedOptions(ClientOptions options, String keyOrToken) throws AblyException {
7172
if (keyOrToken != null) {
72-
return Side.optionsWithSideAgent(keyOrToken, Side.SERVER_AGENT_IDENTIFIER, BuildConfig.VERSION);
73+
return Side.optionsWithSideAgent(keyOrToken, Side.SERVER_AGENT_IDENTIFIER);
7374
}
74-
return Side.optionsWithSideAgent(options, Side.SERVER_AGENT_IDENTIFIER, BuildConfig.VERSION);
75+
return Side.optionsWithSideAgent(options, Side.SERVER_AGENT_IDENTIFIER);
7576
}
7677

7778
/**

‎server/src/test/java/io/ably/pubsub/server/PubSubServerTest.java‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import io.ably.pubsub.internal.Side;
1616
import java.net.InetSocketAddress;
1717
import java.nio.charset.StandardCharsets;
18+
import java.util.Arrays;
1819
import java.util.HashMap;
20+
import java.util.List;
1921
import java.util.Map;
2022
import java.util.concurrent.atomic.AtomicReference;
2123
import org.junit.Test;
@@ -24,6 +26,9 @@
2426
* The agent entries asserted here are what the platform reads to classify traffic (and, on
2527
* MAU-priced accounts, what earns the server exemption), so these tests are deliberately
2628
* strict: if one fails, billing classification is broken, not just a header.
29+
* <p>
30+
* The side entry is a versionless flag — a bare token on the wire, per ably/ably-common#361
31+
* — so the assertions also fail if a version (or any {@code /suffix}) reappears on it.
2732
*/
2833
public class PubSubServerTest {
2934

@@ -36,32 +41,40 @@ private static ClientOptions offlineOptions(String key) throws AblyException {
3641
return options;
3742
}
3843

44+
/** The stamped entry is present as a versionless flag, and the other side's is absent. */
45+
private static void assertServerFlag(Map<String, String> agents) {
46+
assertTrue("expected the server side flag", agents.containsKey(Side.SERVER_AGENT_IDENTIFIER));
47+
assertNull("the side flag is versionless", agents.get(Side.SERVER_AGENT_IDENTIFIER));
48+
assertFalse("a server client must not carry the device entry",
49+
agents.containsKey(Side.DEVICE_AGENT_IDENTIFIER));
50+
}
51+
3952
@Test
4053
public void httpClient_stampsServerAgent() throws AblyException {
4154
AblyRest client = PubSubServer.httpClientBuilder(offlineOptions(FAKE_KEY)).build();
42-
assertEquals(BuildConfig.VERSION, client.options.agents.get(Side.SERVER_AGENT_IDENTIFIER));
55+
assertServerFlag(client.options.agents);
4356
}
4457

4558
@Test
4659
public void realtimeClient_stampsServerAgent() throws AblyException {
4760
AblyRealtime client = PubSubServer.realtimeClientBuilder(offlineOptions(FAKE_KEY)).build();
48-
assertEquals(BuildConfig.VERSION, client.options.agents.get(Side.SERVER_AGENT_IDENTIFIER));
61+
assertServerFlag(client.options.agents);
4962
}
5063

5164
@Test
5265
public void keyString_isAcceptedAndDisambiguatedAsKey() throws AblyException {
5366
AblyRest client = PubSubServer.httpClientBuilder(FAKE_KEY).build();
5467
assertEquals(FAKE_KEY, client.options.key);
5568
assertNull(client.options.token);
56-
assertEquals(BuildConfig.VERSION, client.options.agents.get(Side.SERVER_AGENT_IDENTIFIER));
69+
assertServerFlag(client.options.agents);
5770
}
5871

5972
@Test
6073
public void tokenString_isAcceptedAndDisambiguatedAsToken() throws AblyException {
6174
AblyRest client = PubSubServer.httpClientBuilder(FAKE_TOKEN).build();
6275
assertEquals(FAKE_TOKEN, client.options.token);
6376
assertNull(client.options.key);
64-
assertEquals(BuildConfig.VERSION, client.options.agents.get(Side.SERVER_AGENT_IDENTIFIER));
77+
assertServerFlag(client.options.agents);
6578
}
6679

6780
@Test
@@ -71,16 +84,17 @@ public void callerAgentEntries_arePreserved() throws AblyException {
7184
options.agents.put("some-sdk", "1.2.3");
7285
AblyRest client = PubSubServer.httpClientBuilder(options).build();
7386
assertEquals("1.2.3", client.options.agents.get("some-sdk"));
74-
assertEquals(BuildConfig.VERSION, client.options.agents.get(Side.SERVER_AGENT_IDENTIFIER));
87+
assertServerFlag(client.options.agents);
7588
}
7689

7790
@Test
7891
public void callerCannotOverrideTheSideEntry() throws AblyException {
7992
ClientOptions options = offlineOptions(FAKE_KEY);
8093
options.agents = new HashMap<>();
81-
options.agents.put(Side.SERVER_AGENT_IDENTIFIER, "not-the-real-version");
94+
options.agents.put(Side.SERVER_AGENT_IDENTIFIER, "not-the-real-form");
8295
AblyRest client = PubSubServer.httpClientBuilder(options).build();
83-
assertEquals(BuildConfig.VERSION, client.options.agents.get(Side.SERVER_AGENT_IDENTIFIER));
96+
// The stamp replaces the caller's value: the flag is present and back to versionless.
97+
assertServerFlag(client.options.agents);
8498
}
8599

86100
@Test
@@ -107,8 +121,8 @@ public void nullOptions_getTheCoreConstructorsOwnError() {
107121

108122
/**
109123
* Wire-level assertion: the Ably-Agent header actually sent over HTTP carries the
110-
* side-declaring entry alongside the core's base identifier. This is the value billing
111-
* classification reads.
124+
* side-declaring flag as a bare token alongside the core's base identifier. This is the
125+
* value billing classification reads.
112126
*/
113127
@Test
114128
public void httpRequests_carryTheServerAgentHeaderOnTheWire() throws Exception {
@@ -133,8 +147,13 @@ public void httpRequests_carryTheServerAgentHeaderOnTheWire() throws Exception {
133147

134148
String agentHeader = observedAgentHeader.get();
135149
assertNotNull("no Ably-Agent header observed", agentHeader);
136-
assertTrue("missing side-declaring entry in: " + agentHeader,
137-
agentHeader.contains(Side.SERVER_AGENT_IDENTIFIER + "/" + BuildConfig.VERSION));
150+
List<String> tokens = Arrays.asList(agentHeader.split(" "));
151+
// The flag must be present as a bare token: `name/anything` means the
152+
// versionless stamp regressed (see ably/ably-common#361).
153+
assertTrue("missing bare side flag in: " + agentHeader,
154+
tokens.contains(Side.SERVER_AGENT_IDENTIFIER));
155+
assertFalse("side flag must be versionless in: " + agentHeader,
156+
agentHeader.contains(Side.SERVER_AGENT_IDENTIFIER + "/"));
138157
assertTrue("missing core base identifier in: " + agentHeader,
139158
agentHeader.contains("ably-java/"));
140159
} finally {

‎shared/src/main/java/io/ably/pubsub/internal/Side.java‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ private Side() {}
4545
* Returns a copy of the caller's options carrying the agent entry that declares this
4646
* package's side.
4747
* <p>
48+
* The side entry is a <em>versionless flag</em> — a bare token on the wire, like the
49+
* platform's own {@code browser} entry — registered as such in the ably-common agents
50+
* registry (see ably/ably-common#361). Identity, version and support status keep
51+
* travelling on the SDK's own {@code ably-java/<version>} entry alongside it;
52+
* {@link io.ably.lib.util.AgentHeaderCreator} emits a map entry with a {@code null}
53+
* value as a bare token.
54+
* <p>
4855
* The copy is made with {@link ClientOptions#copy()} and a fresh agents map, so the
4956
* caller's options and their own {@code agents} map are both left untouched. The
5057
* caller's {@code agents} entries are preserved alongside the side stamp, so an SDK
@@ -59,10 +66,9 @@ private Side() {}
5966
*
6067
* @param options the options the caller passed to the door's builder, or {@code null}.
6168
* @param identifier the side-declaring agent identifier to stamp.
62-
* @param version the version of the package doing the stamping.
6369
* @return a stamped copy of the options, or {@code null} if {@code options} was {@code null}.
6470
*/
65-
public static ClientOptions optionsWithSideAgent(ClientOptions options, String identifier, String version) {
71+
public static ClientOptions optionsWithSideAgent(ClientOptions options, String identifier) {
6672
if (options == null) {
6773
return null;
6874
}
@@ -71,28 +77,27 @@ public static ClientOptions optionsWithSideAgent(ClientOptions options, String i
7177
if (options.agents != null) {
7278
agents.putAll(options.agents);
7379
}
74-
agents.put(identifier, version);
80+
agents.put(identifier, null);
7581
stamped.agents = agents;
7682
return stamped;
7783
}
7884

7985
/**
80-
* As {@link #optionsWithSideAgent(ClientOptions, String, String)}, for the API key or
86+
* As {@link #optionsWithSideAgent(ClientOptions, String)}, for the API key or
8187
* token string form the core constructors also accept. Reuses the core's own
8288
* key-versus-token disambiguation ({@link ClientOptions#ClientOptions(String)}: an Ably
8389
* API key always contains a colon, an Ably token never does).
8490
*
8591
* @param keyOrToken the Ably API key or token string the caller passed to the door's builder.
8692
* @param identifier the side-declaring agent identifier to stamp.
87-
* @param version the version of the package doing the stamping.
8893
* @return stamped options constructed from the key or token.
8994
* @throws AblyException if the key or token string is rejected by the core.
9095
*/
91-
public static ClientOptions optionsWithSideAgent(String keyOrToken, String identifier, String version)
96+
public static ClientOptions optionsWithSideAgent(String keyOrToken, String identifier)
9297
throws AblyException {
9398
ClientOptions options = new ClientOptions(keyOrToken);
9499
options.agents = new LinkedHashMap<>();
95-
options.agents.put(identifier, version);
100+
options.agents.put(identifier, null);
96101
return options;
97102
}
98103
}

0 commit comments

Comments
 (0)