From 145c4017edc5410764c20f996825880e00d6fae6 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 4 May 2026 10:05:23 -0400 Subject: [PATCH 1/3] fix: validate non-empty flag key and non-null defaultValue in Variable Match the existing guards in the Cloud client and the Java/Python SDKs. Empty/null keys reaching the WASM bucketing engine trigger an internal abort() at eventQueue.ts:122 ('Event missing target to save aggregate event') which compiles to a wasmtime trap. After enough traps the AS heap state becomes corrupted, and on the .NET host wasmtime panics internally. --- .../DevCycleTest.cs | 53 ++++++++++++++++++- .../Api/DevCycleLocalClient.cs | 20 +++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs b/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs index a276a47..ad82ad1 100644 --- a/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs +++ b/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using DevCycle.SDK.Server.Local.Api; using DevCycle.SDK.Server.Common.Model; @@ -233,6 +233,57 @@ public void Variable_NullUser_ThrowsException() }); } + [TestMethod] + public void Variable_NullKey_ThrowsArgumentException() + { + // Reaching the WASM bucketing engine with a null/empty flag key + // triggers an internal abort() and corrupts the WASM heap. Match + // the Java/Python SDKs (and Cloud client) by failing fast with a + // clear ArgumentException before we ever enter WASM. + using DevCycleLocalClient api = DevCycleTestClient.getTestClient(); + var user = new DevCycleUser("test_user"); + + Assert.Throws(() => api.Variable(user, null, true).Result); + } + + [TestMethod] + public void Variable_EmptyKey_ThrowsArgumentException() + { + using DevCycleLocalClient api = DevCycleTestClient.getTestClient(); + var user = new DevCycleUser("test_user"); + + Assert.Throws(() => api.Variable(user, "", true).Result); + } + + [TestMethod] + public async Task VariableAsync_NullKey_ThrowsArgumentException() + { + using DevCycleLocalClient api = DevCycleTestClient.getTestClient(); + var user = new DevCycleUser("test_user"); + + await Assert.ThrowsExactlyAsync(async () => + await api.VariableAsync(user, null, true)); + } + + [TestMethod] + public async Task VariableAsync_EmptyKey_ThrowsArgumentException() + { + using DevCycleLocalClient api = DevCycleTestClient.getTestClient(); + var user = new DevCycleUser("test_user"); + + await Assert.ThrowsExactlyAsync(async () => + await api.VariableAsync(user, "", true)); + } + + [TestMethod] + public void Variable_NullDefaultValue_ThrowsArgumentNullException() + { + using DevCycleLocalClient api = DevCycleTestClient.getTestClient(); + var user = new DevCycleUser("test_user"); + + Assert.Throws(() => api.Variable(user, "some_key", null).Result); + } + [TestMethod] public void User_NullUserId_ThrowsException() { diff --git a/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs b/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs index 1c579fb..2fd7fbc 100644 --- a/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs +++ b/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs @@ -330,6 +330,16 @@ public override Task> Variable(DevCycleUser user, string key, T d { var requestUser = new DevCyclePopulatedUser(user); + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("key cannot be null or empty"); + } + + if (defaultValue == null) + { + throw new ArgumentNullException(nameof(defaultValue)); + } + if (!configManager.Initialized) { logger.LogWarning("Variable called before DevCycleClient has initialized, returning default value"); @@ -389,6 +399,16 @@ public async Task> VariableAsync(DevCycleUser user, string key, T { var requestUser = new DevCyclePopulatedUser(user); + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("key cannot be null or empty"); + } + + if (defaultValue == null) + { + throw new ArgumentNullException(nameof(defaultValue)); + } + if (!configManager.Initialized) { logger.LogWarning("Variable called before DevCycleClient has initialized, returning default value"); From f49e7a82e74b3aed714bfa55fa7cf352f930b939 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 4 May 2026 10:16:45 -0400 Subject: [PATCH 2/3] test: add VariableAsync null defaultValue test --- DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs b/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs index ad82ad1..26265bc 100644 --- a/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs +++ b/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs @@ -284,6 +284,16 @@ public void Variable_NullDefaultValue_ThrowsArgumentNullException() Assert.Throws(() => api.Variable(user, "some_key", null).Result); } + [TestMethod] + public async Task VariableAsync_NullDefaultValue_ThrowsArgumentNullException() + { + using DevCycleLocalClient api = DevCycleTestClient.getTestClient(); + var user = new DevCycleUser("test_user"); + + await Assert.ThrowsExactlyAsync(async () => + await api.VariableAsync(user, "some_key", null)); + } + [TestMethod] public void User_NullUserId_ThrowsException() { From ca3e4ae655dd76ecd14a1ff591fde4ffa65ac52a Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Fri, 7 Aug 2026 14:32:37 -0400 Subject: [PATCH 3/3] fix: allow null defaultValue for JSON variables --- .../Api/DevCycleCloudClient.cs | 5 ----- DevCycle.SDK.Server.Common/Model/Variable.cs | 4 +++- .../DevCycleTest.cs | 16 +++++++++++----- .../Api/DevCycleLocalClient.cs | 10 ---------- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/DevCycle.SDK.Server.Cloud/Api/DevCycleCloudClient.cs b/DevCycle.SDK.Server.Cloud/Api/DevCycleCloudClient.cs index d81602b..acb6204 100644 --- a/DevCycle.SDK.Server.Cloud/Api/DevCycleCloudClient.cs +++ b/DevCycle.SDK.Server.Cloud/Api/DevCycleCloudClient.cs @@ -103,11 +103,6 @@ public override async Task> Variable(DevCycleUser user, string ke throw new ArgumentException("key cannot be null or empty"); } - if (defaultValue == null) - { - throw new ArgumentNullException(nameof(defaultValue)); - } - AddDefaults(user); string lowerKey = key.ToLower(); diff --git a/DevCycle.SDK.Server.Common/Model/Variable.cs b/DevCycle.SDK.Server.Common/Model/Variable.cs index 7cbdc2e..85539a5 100644 --- a/DevCycle.SDK.Server.Common/Model/Variable.cs +++ b/DevCycle.SDK.Server.Common/Model/Variable.cs @@ -129,7 +129,9 @@ public static TypeEnum DetermineType(T variableValue) try { - var baseType = variableValue.GetType(); + // A null default value is legitimate for JSON variables (and nullable strings), + // so fall back to the declared type rather than dereferencing the value. + var baseType = variableValue?.GetType() ?? typeof(T); if (baseType == typeof(string)) { diff --git a/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs b/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs index 26265bc..06921bf 100644 --- a/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs +++ b/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs @@ -4,6 +4,7 @@ using DevCycle.SDK.Server.Common.Model; using DevCycle.SDK.Server.Common.Model.Local; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; using Environment = System.Environment; using System.Collections.Generic; using System.Text.Json; @@ -276,22 +277,27 @@ await Assert.ThrowsExactlyAsync(async () => } [TestMethod] - public void Variable_NullDefaultValue_ThrowsArgumentNullException() + public async Task Variable_NullJsonDefaultValue_IsAllowed() { + // A null default is legitimate for JSON variables, so the key validation + // above must not be extended to defaultValue. using DevCycleLocalClient api = DevCycleTestClient.getTestClient(); var user = new DevCycleUser("test_user"); - Assert.Throws(() => api.Variable(user, "some_key", null).Result); + var variable = await api.Variable(user, "some_key", null); + + Assert.IsNotNull(variable); } [TestMethod] - public async Task VariableAsync_NullDefaultValue_ThrowsArgumentNullException() + public async Task VariableAsync_NullJsonDefaultValue_IsAllowed() { using DevCycleLocalClient api = DevCycleTestClient.getTestClient(); var user = new DevCycleUser("test_user"); - await Assert.ThrowsExactlyAsync(async () => - await api.VariableAsync(user, "some_key", null)); + var variable = await api.VariableAsync(user, "some_key", null); + + Assert.IsNotNull(variable); } [TestMethod] diff --git a/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs b/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs index 2fd7fbc..19d683f 100644 --- a/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs +++ b/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs @@ -335,11 +335,6 @@ public override Task> Variable(DevCycleUser user, string key, T d throw new ArgumentException("key cannot be null or empty"); } - if (defaultValue == null) - { - throw new ArgumentNullException(nameof(defaultValue)); - } - if (!configManager.Initialized) { logger.LogWarning("Variable called before DevCycleClient has initialized, returning default value"); @@ -404,11 +399,6 @@ public async Task> VariableAsync(DevCycleUser user, string key, T throw new ArgumentException("key cannot be null or empty"); } - if (defaultValue == null) - { - throw new ArgumentNullException(nameof(defaultValue)); - } - if (!configManager.Initialized) { logger.LogWarning("Variable called before DevCycleClient has initialized, returning default value");