diff --git a/appveyor.yml b/appveyor.yml index 84b48a45..c37344e3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ environment: - build_version: 9.0.0 + build_version: 9.0.1 version: $(build_version)-{build} image: Visual Studio 2022 configuration: Release diff --git a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs index 43e0bd44..7dd29dad 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs @@ -460,4 +460,19 @@ public void Ensure_Multiple_Requests_Doesnt_Interfere_In_ValueTasks() manualPollClient.ForceRefresh(); }); } + + [DataTestMethod] + [DataRow("specialCharacters", "äöüÄÖÜçéèñışğ⢙✓😀", "äöüÄÖÜçéèñışğ⢙✓😀")] + [DataRow("specialCharactersHashed", "äöüÄÖÜçéèñışğ⢙✓😀", "äöüÄÖÜçéèñışğ⢙✓😀")] + public async Task SpecialCharacters_Works(string settingKey, string userId, string expectedValue) + { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dc016a-675e-4aa2-8492-6f572ad98037/244cf8b0-f604-11e8-b543-f23c917f9d8d + using var client = ConfigCatClient.Get("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g", options => + { + options.PollingMode = PollingModes.LazyLoad(); + }); + + var actual = await client.GetValueAsync(settingKey, "NOT_CAT", new User(userId)); + Assert.AreEqual(expectedValue, actual); + } } diff --git a/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs index 6135fa9f..bddd5e54 100644 --- a/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs @@ -55,7 +55,7 @@ public class SensitiveTestsDescriptor : IMatrixTestDescriptor public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } - public class VariationIdTestsDescriptor : IMatrixTestDescriptor, IVariationIdMatrixText + public class VariationIdTestsDescriptor : IMatrixTestDescriptor, IVariationIdMatrixTest { // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d774b9-3d05-0027-d5f4-3e76c3dba752/244cf8b0-f604-11e8-b543-f23c917f9d8d public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA"); diff --git a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs index 9ee1bde3..ddb980ce 100644 --- a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs @@ -64,7 +64,7 @@ public class SensitiveTestsDescriptor : IMatrixTestDescriptor public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } - public class VariationIdTestsDescriptor : IMatrixTestDescriptor, IVariationIdMatrixText + public class VariationIdTestsDescriptor : IMatrixTestDescriptor, IVariationIdMatrixTest { // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-30c6-4969-8e4c-03f6a8764199/244cf8b0-f604-11e8-b543-f23c917f9d8d public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/spQnkRTIPEWVivZkWM84lQ"); diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs index 798831a9..33839706 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs @@ -23,7 +23,7 @@ public interface IMatrixTestDescriptor public string MatrixResultFileName { get; } } -public interface IVariationIdMatrixText { } +public interface IVariationIdMatrixTest { } public class MatrixTestRunnerBase where TDescriptor : IMatrixTestDescriptor, new() { @@ -34,7 +34,7 @@ public interface IVariationIdMatrixText { } public MatrixTestRunnerBase() { - this.isVariationIdMatrixTest = DescriptorInstance is IVariationIdMatrixText; + this.isVariationIdMatrixTest = DescriptorInstance is IVariationIdMatrixTest; this.config = DescriptorInstance.ConfigLocation.FetchConfigCached().Settings; } diff --git a/src/ConfigCatClient/HttpConfigFetcher.cs b/src/ConfigCatClient/HttpConfigFetcher.cs index 9e109d48..a015411c 100644 --- a/src/ConfigCatClient/HttpConfigFetcher.cs +++ b/src/ConfigCatClient/HttpConfigFetcher.cs @@ -5,12 +5,6 @@ using System.Threading; using System.Threading.Tasks; -#if NET45 -using ResponseWithBody = System.Tuple; -#else -using ResponseWithBody = System.ValueTuple; -#endif - namespace ConfigCat.Client; internal sealed class HttpConfigFetcher : IConfigFetcher, IDisposable @@ -85,22 +79,24 @@ private async ValueTask FetchInternalAsync(ProjectConfig lastConfig { var responseWithBody = await FetchRequestAsync(lastConfig.HttpETag, this.requestUri).ConfigureAwait(false); - var response = responseWithBody.Item1; + var response = responseWithBody.Response; switch (response.StatusCode) { case HttpStatusCode.OK: - if (responseWithBody.Item2 is null) + var config = responseWithBody.Config; + if (config is null) { - logMessage = this.logger.FetchReceived200WithInvalidBody(); - return FetchResult.Failure(lastConfig, logMessage.InvariantFormattedMessage); + var exception = responseWithBody.Exception; + logMessage = this.logger.FetchReceived200WithInvalidBody(exception); + return FetchResult.Failure(lastConfig, logMessage.InvariantFormattedMessage, exception); } return FetchResult.Success(new ProjectConfig ( httpETag: response.Headers.ETag?.Tag, - configJson: responseWithBody.Item2, - config: responseWithBody.Item3, + configJson: responseWithBody.ResponseBody, + config: config, timeStamp: ProjectConfig.GenerateTimeStamp() )); @@ -177,11 +173,9 @@ private async ValueTask FetchRequestAsync(string? httpETag, Ur var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); #endif - var config = responseBody.DeserializeOrDefault(); - if (config is null) - { - return new ResponseWithBody(response, null, null); - } + Config config; + try { config = Config.Deserialize(responseBody.AsMemory()); } + catch (Exception ex) { return new ResponseWithBody(response, responseBody, ex); } if (config.Preferences is not null) { @@ -224,7 +218,7 @@ private async ValueTask FetchRequestAsync(string? httpETag, Ur return new ResponseWithBody(response, responseBody, config); } - return new ResponseWithBody(response, null, null); + return new ResponseWithBody(response, null, (Exception?)null); } } @@ -277,4 +271,28 @@ public void Dispose() this.cancellationTokenSource.Cancel(); this.httpClient?.Dispose(); } + + private readonly struct ResponseWithBody + { + private readonly object? configOrException; + + public ResponseWithBody(HttpResponseMessage response, string responseBody, Config config) + { + Response = response; + ResponseBody = responseBody; + this.configOrException = config; + } + + public ResponseWithBody(HttpResponseMessage response, string? responseBody, Exception? exception) + { + Response = response; + ResponseBody = responseBody; + this.configOrException = exception; + } + + public HttpResponseMessage Response { get; } + public string? ResponseBody { get; } + public Config? Config => this.configOrException as Config; + public Exception? Exception => this.configOrException as Exception; + } } diff --git a/src/ConfigCatClient/Logging/LogMessages.cs b/src/ConfigCatClient/Logging/LogMessages.cs index f523a6c8..2cb94686 100644 --- a/src/ConfigCatClient/Logging/LogMessages.cs +++ b/src/ConfigCatClient/Logging/LogMessages.cs @@ -53,14 +53,14 @@ public static FormattableLogMessage FetchFailedDueToRequestTimeout(this LoggerWr public static FormattableLogMessage FetchFailedDueToUnexpectedError(this LoggerWrapper logger, Exception ex) => logger.Log( LogLevel.Error, 1103, ex, - "Unexpected error occurred while trying to fetch config JSON."); + "Unexpected error occurred while trying to fetch config JSON. It is most likely due to a local network issue. Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) over HTTP."); public static FormattableLogMessage FetchFailedDueToRedirectLoop(this LoggerWrapper logger) => logger.Log( LogLevel.Error, 1104, "Redirection loop encountered while trying to fetch config JSON. Please contact us at https://configcat.com/support/"); - public static FormattableLogMessage FetchReceived200WithInvalidBody(this LoggerWrapper logger) => logger.Log( - LogLevel.Error, 1105, + public static FormattableLogMessage FetchReceived200WithInvalidBody(this LoggerWrapper logger, Exception? ex) => logger.Log( + LogLevel.Error, 1105, ex, "Fetching config JSON was successful but the HTTP response content was invalid."); public static FormattableLogMessage FetchReceived304WhenLocalCacheIsEmpty(this LoggerWrapper logger, int statusCode, string? reasonPhrase) => logger.LogInterpolated( diff --git a/src/ConfigCatClient/Models/Config.cs b/src/ConfigCatClient/Models/Config.cs index 98f19491..37cebd0d 100644 --- a/src/ConfigCatClient/Models/Config.cs +++ b/src/ConfigCatClient/Models/Config.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -39,6 +40,12 @@ internal sealed class Config : IConfig , IJsonOnDeserialized #endif { + public static Config Deserialize(ReadOnlyMemory configJson, bool tolerant = false) + { + return configJson.Deserialize(tolerant) + ?? throw new ArgumentException("Invalid config JSON content: " + configJson, nameof(configJson)); + } + #if USE_NEWTONSOFT_JSON [JsonProperty(PropertyName = "p")] #else diff --git a/src/ConfigCatClient/Models/SegmentComparator.cs b/src/ConfigCatClient/Models/SegmentComparator.cs index 561cbe41..2a411cf8 100644 --- a/src/ConfigCatClient/Models/SegmentComparator.cs +++ b/src/ConfigCatClient/Models/SegmentComparator.cs @@ -8,10 +8,10 @@ public enum SegmentComparator : byte /// /// IS IN SEGMENT - It matches when the conditions of the specified segment are evaluated to true. /// - IsIn, + IsIn = 0, /// /// IS NOT IN SEGMENT - It matches when the conditions of the specified segment are evaluated to false. /// - IsNotIn, + IsNotIn = 1, } diff --git a/src/ConfigCatClient/Override/LocalFileDataSource.cs b/src/ConfigCatClient/Override/LocalFileDataSource.cs index e089175c..bb436e02 100644 --- a/src/ConfigCatClient/Override/LocalFileDataSource.cs +++ b/src/ConfigCatClient/Override/LocalFileDataSource.cs @@ -111,8 +111,7 @@ private async Task ReloadFileAsync(bool isAsync, CancellationToken cancellationT break; } - var deserialized = content.Deserialize(tolerant: true) - ?? throw new InvalidOperationException("Invalid config JSON content: " + content); + var deserialized = Config.Deserialize(content.AsMemory(), tolerant: true); this.overrideValues = deserialized.Settings; break; } diff --git a/src/ConfigCatClient/ProjectConfig.cs b/src/ConfigCatClient/ProjectConfig.cs index fb93389c..50c496af 100644 --- a/src/ConfigCatClient/ProjectConfig.cs +++ b/src/ConfigCatClient/ProjectConfig.cs @@ -87,11 +87,7 @@ public static ProjectConfig Deserialize(string value) string? configJson; if (configJsonSpan.Length > 0) { - config = configJsonSpan.DeserializeOrDefault(); - if (config is null) - { - throw new FormatException("Invalid config JSON content: " + configJsonSpan.ToString()); - } + config = Config.Deserialize(configJsonSpan); configJson = configJsonSpan.ToString(); } else