diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26a78f4..5641b51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ logback = "1.5.19" mockwebserver = "4.12.0" mockito = "5.14.2" android-retrofuture = "1.7.4" -android-gradle = "8.0.0" +android-gradle = "8.2.0" [libraries] slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 87b5be7..20dfef1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Aug 04 20:00:01 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/com/configcat/ConfigCatClient.java b/src/main/java/com/configcat/ConfigCatClient.java index 69d0b12..3033348 100644 --- a/src/main/java/com/configcat/ConfigCatClient.java +++ b/src/main/java/com/configcat/ConfigCatClient.java @@ -57,8 +57,8 @@ private ConfigCatClient(String sdkKey, Options options) throws IllegalArgumentEx monitor = options.context != null ? new AppStateMonitor(options.context, logger) : null; this.configService = new ConfigService(sdkKey, monitor, options.pollingMode, options.cache, logger, fetcher, options.hooks, options.offline); } catch (Exception e) { - if(fetcher != null) fetcher.close(); - if(monitor != null) monitor.close(); + if (fetcher != null) fetcher.close(); + if (monitor != null) monitor.close(); throw e; } } else { @@ -83,9 +83,13 @@ public T getValue(Class classOfT, String key, User user, T defaultValue) } catch (InterruptedException e) { this.logger.error(0, "Thread interrupted.", e); Thread.currentThread().interrupt(); + EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR, e.getMessage(), e, user); + this.hooks.invokeOnFlagEvaluated(evaluationDetails); return defaultValue; } catch (Exception e) { this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValue", key, "defaultValue", defaultValue.toString()), e); + EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.fromException(e), e.getMessage(), e, user); + this.hooks.invokeOnFlagEvaluated(evaluationDetails); return defaultValue; } } @@ -130,10 +134,14 @@ public EvaluationDetails getValueDetails(Class classOfT, String key, U String error = "Thread interrupted."; this.logger.error(0, error, e); Thread.currentThread().interrupt(); - return EvaluationDetails.fromError(key, defaultValue, error + ": " + e.getMessage(), user); + EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR, error + ": " + e.getMessage(), e, user); + this.hooks.invokeOnFlagEvaluated(evaluationDetails); + return evaluationDetails.asTypeSpecific(); } catch (Exception e) { this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e); - return EvaluationDetails.fromError(key, defaultValue, e.getMessage(), user); + EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.fromException(e), e.getMessage(), e, user); + this.hooks.invokeOnFlagEvaluated(evaluationDetails); + return evaluationDetails.asTypeSpecific(); } } @@ -151,15 +159,23 @@ public CompletableFuture> getValueDetailsAsync(Class return this.getSettingsAsync() .thenApply(settingsResult -> { - Result checkSettingResult = checkSettingAvailable(settingsResult, key, defaultValue); - if (checkSettingResult.error() != null) { - EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, checkSettingResult.error(), user); + try { + Result checkSettingResult = checkSettingAvailable(settingsResult, key, defaultValue); + if (checkSettingResult.error() != null) { + EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, checkSettingResult.errorCode(), checkSettingResult.error(), null, user); + this.hooks.invokeOnFlagEvaluated(evaluationDetails); + return evaluationDetails.asTypeSpecific(); + } + return this.evaluate(classOfT, checkSettingResult.value(), + key, user != null ? user : this.defaultUser, settingsResult.fetchTime(), settingsResult.settings()); + } catch (Exception e) { + this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetailsAsync", key, "defaultValue", defaultValue), e); + EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.fromException(e), e.getMessage(), e, user); this.hooks.invokeOnFlagEvaluated(evaluationDetails); return evaluationDetails.asTypeSpecific(); } - return this.evaluate(classOfT, checkSettingResult.value(), - key, user != null ? user : this.defaultUser, settingsResult.fetchTime(), settingsResult.settings()); }); + } @Override @@ -270,6 +286,8 @@ public Map.Entry getKeyAndValue(Class classOfT, String variati if (variationId == null || variationId.isEmpty()) throw new IllegalArgumentException("'variationId' cannot be null or empty."); + validateReturnType(classOfT); + try { return this.getKeyAndValueAsync(classOfT, variationId).get(); } catch (InterruptedException e) { @@ -287,6 +305,8 @@ public CompletableFuture> getKeyAndValueAsync(Class if (variationId == null || variationId.isEmpty()) throw new IllegalArgumentException("'variationId' cannot be null or empty."); + validateReturnType(classOfT); + return this.getSettingsAsync() .thenApply(settingsResult -> this.getKeyAndValueFromSettingsMap(classOfT, settingsResult, variationId)); } @@ -328,17 +348,19 @@ public RefreshResult forceRefresh() { } catch (InterruptedException e) { logger.error(0, "Thread interrupted.", e); Thread.currentThread().interrupt(); + return new RefreshResult(false, "An error occurred during the refresh.", RefreshErrorCode.UNEXPECTED_ERROR, e); } catch (Exception e) { this.logger.error(1003, ConfigCatLogMessages.getForceRefreshError("forceRefresh"), e); + return new RefreshResult(false, "An error occurred during the refresh.", RefreshErrorCode.UNEXPECTED_ERROR, e); } - return new RefreshResult(false, "An error occurred during the refresh."); + } @Override public CompletableFuture forceRefreshAsync() { if (configService == null) { return CompletableFuture.completedFuture(new RefreshResult(false, - "The ConfigCat SDK is in local-only mode. Calling .forceRefresh() has no effect.")); + "The ConfigCat SDK is in local-only mode. Calling .forceRefresh() has no effect.", RefreshErrorCode.LOCAL_ONLY_CLIENT, null)); } return configService.refresh(); @@ -439,16 +461,16 @@ private CompletableFuture getSettingsAsync() { private T getValueFromSettingsMap(Class classOfT, SettingResult settingResult, String key, User user, T defaultValue) { User userObject = user != null ? user : this.defaultUser; try { - Result checkSettingResult = checkSettingAvailable(settingResult, key, defaultValue); + Result checkSettingResult = checkSettingAvailable(settingResult, key, defaultValue); if (checkSettingResult.error() != null) { - this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, checkSettingResult.error(), user)); + this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, checkSettingResult.errorCode(), checkSettingResult.error(), null, user)); return defaultValue; } return this.evaluate(classOfT, checkSettingResult.value(), key, userObject, settingResult.fetchTime(), settingResult.settings()).getValue(); } catch (Exception | NoSuchMethodError e) { FormattableLogMessage error = ConfigCatLogMessages.getSettingEvaluationFailedForOtherReason(key, "defaultValue", defaultValue); - this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, error + " " + e.getMessage(), userObject)); this.logger.error(2001, error, e); + this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.fromException(e), error + " " + e.getMessage(), e, userObject)); return defaultValue; } } @@ -466,7 +488,7 @@ private Map.Entry getKeyAndValueFromSettingsMap(Class classOfT return new AbstractMap.SimpleEntry<>(settingKey, (T) this.parseObject(classOfT, setting.getSettingsValue(), setting.getType())); } - if(setting.getTargetingRules() != null) { + if (setting.getTargetingRules() != null) { for (TargetingRule targetingRule : setting.getTargetingRules()) { if (targetingRule.getSimpleValue() != null) { if (variationId.equals(targetingRule.getSimpleValue().getVariationId())) { @@ -485,7 +507,7 @@ private Map.Entry getKeyAndValueFromSettingsMap(Class classOfT } } - if( setting.getPercentageOptions() != null) { + if (setting.getPercentageOptions() != null) { for (PercentageOption percentageOption : setting.getPercentageOptions()) { if (variationId.equals(percentageOption.getVariationId())) { return new AbstractMap.SimpleEntry<>(settingKey, (T) this.parseObject(classOfT, percentageOption.getValue(), setting.getType())); @@ -510,6 +532,8 @@ private EvaluationDetails evaluate(Class classOfT, Setting setting, St user, false, null, + EvaluationErrorCode.NONE, + null, fetchTime, evaluationResult.targetingRule, evaluationResult.percentageOption); @@ -518,8 +542,6 @@ private EvaluationDetails evaluate(Class classOfT, Setting setting, St } private Object parseObject(Class classOfT, SettingValue settingValue, SettingType settingType) { - validateReturnType(classOfT); - if (classOfT == String.class && settingValue.getStringValue() != null && SettingType.STRING.equals(settingType)) return settingValue.getStringValue(); else if ((classOfT == Integer.class || classOfT == int.class) && settingValue.getIntegerValue() != null && SettingType.INT.equals(settingType)) @@ -529,7 +551,7 @@ else if ((classOfT == Double.class || classOfT == double.class) && settingValue. else if ((classOfT == Boolean.class || classOfT == boolean.class) && settingValue.getBooleanValue() != null && SettingType.BOOLEAN.equals(settingType)) return settingValue.getBooleanValue(); - throw new IllegalArgumentException("The type of a setting must match the type of the specified default value. " + throw new EvaluationException("The type of a setting must match the type of the specified default value. " + "Setting's type was {" + settingType + "} but the default value's type was {" + classOfT + "}. " + "Please use a default value which corresponds to the setting type {" + settingType + "}." + "Learn more: https://configcat.com/docs/sdk-reference/android/#setting-type-mapping"); @@ -545,7 +567,7 @@ else if (settingType == SettingType.INT) else if (settingType == SettingType.DOUBLE) return double.class; else - throw new IllegalArgumentException("Only String, Integer, Double or Boolean types are supported"); + throw new InvalidConfigModelException("Only String, Integer, Double or Boolean types are supported"); } private boolean checkSettingsAvailable(SettingResult settingResult, String emptyResult) { @@ -557,11 +579,11 @@ private boolean checkSettingsAvailable(SettingResult settingResult, String empty return true; } - private Result checkSettingAvailable(SettingResult settingResult, String key, T defaultValue) { + private Result checkSettingAvailable(SettingResult settingResult, String key, T defaultValue) { if (settingResult.isEmpty()) { FormattableLogMessage errorMessage = ConfigCatLogMessages.getConfigJsonIsNotPresentedWithDefaultValue(key, "defaultValue", defaultValue); this.logger.error(1000, errorMessage); - return Result.error(errorMessage, null); + return Result.error(errorMessage, null, EvaluationErrorCode.CONFIG_JSON_NOT_AVAILABLE, null); } Map settings = settingResult.settings(); @@ -569,10 +591,10 @@ private Result checkSettingAvailable(SettingResult settingResult, S if (setting == null) { FormattableLogMessage errorMessage = ConfigCatLogMessages.getSettingEvaluationFailedDueToMissingKey(key, "defaultValue", defaultValue, settings.keySet()); this.logger.error(1001, errorMessage); - return Result.error(errorMessage, null); + return Result.error(errorMessage, null, EvaluationErrorCode.SETTING_KEY_MISSING, null); } - return Result.success(setting); + return Result.success(setting, EvaluationErrorCode.NONE); } /** diff --git a/src/main/java/com/configcat/ConfigFetcher.java b/src/main/java/com/configcat/ConfigFetcher.java index a6bebbd..d094ff5 100644 --- a/src/main/java/com/configcat/ConfigFetcher.java +++ b/src/main/java/com/configcat/ConfigFetcher.java @@ -22,6 +22,8 @@ public enum Status { private final Status status; private final Entry entry; private final Object error; + private final RefreshErrorCode errorCode; + private final Throwable errorException; private final boolean fetchTimeUpdatable; private final String cfRayId; @@ -49,26 +51,32 @@ public Object error() { return this.error; } + public RefreshErrorCode errorCode() {return this.errorCode;} + + public Throwable errorException() {return this.errorException;} + public String cfRayId() {return this.cfRayId;} - FetchResponse(Status status, Entry entry, Object error, boolean fetchTimeUpdatable, String cfRayId) { + FetchResponse(Status status, Entry entry, Object error, RefreshErrorCode errorCode,Throwable errorException, boolean fetchTimeUpdatable, String cfRayId) { this.status = status; this.entry = entry; this.error = error; + this.errorCode = errorCode; + this.errorException = errorException; this.fetchTimeUpdatable = fetchTimeUpdatable; this.cfRayId = cfRayId; } public static FetchResponse fetched(Entry entry, String cfRayId) { - return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, false, cfRayId); + return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, RefreshErrorCode.NONE, null, false, cfRayId); } public static FetchResponse notModified(String cfRayId) { - return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, true, cfRayId); + return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, RefreshErrorCode.NONE, null, true, cfRayId); } - public static FetchResponse failed(Object error, boolean fetchTimeUpdatable, String cfRayId) { - return new FetchResponse(Status.FAILED, Entry.EMPTY, error, fetchTimeUpdatable, cfRayId); + public static FetchResponse failed(Object error, RefreshErrorCode errorCode, Throwable errorException, boolean fetchTimeUpdatable, String cfRayId) { + return new FetchResponse(Status.FAILED, Entry.EMPTY, error,errorCode, errorException, fetchTimeUpdatable, cfRayId); } } @@ -190,9 +198,9 @@ private void callHTTP(String previousETag, CompletableFuture resu if (responseCode == 200) { String content = readBody(urlConnection.getInputStream()); String eTag = readHeaderValue(responseHeaders,"ETag"); - Result configResult = deserializeConfig(content, cfRayId); + Result configResult = deserializeConfig(content, cfRayId); if (configResult.error() != null) { - fetchResponse = FetchResponse.failed(configResult.error(), false, cfRayId); + fetchResponse = FetchResponse.failed(configResult.error(), configResult.errorCode(), configResult.errorException(), false, cfRayId); } else { logger.debug("Fetch was successful: new config fetched."); fetchResponse = FetchResponse.fetched(new Entry(configResult.value(), eTag, content, System.currentTimeMillis()), cfRayId); @@ -207,24 +215,24 @@ private void callHTTP(String previousETag, CompletableFuture resu } else if (responseCode == 403 || responseCode == 404) { FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToInvalidSDKKey(cfRayId); logger.error(1100, message); - fetchResponse = FetchResponse.failed(message, true, cfRayId); + fetchResponse = FetchResponse.failed(message, RefreshErrorCode.INVALID_SDK_KEY, null, true, cfRayId); } else { FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToUnexpectedHttpResponse(responseCode, urlConnection.getResponseMessage(), cfRayId); logger.error(1101, message); - fetchResponse = FetchResponse.failed(message, false, cfRayId); + fetchResponse = FetchResponse.failed(message, RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, null,false, cfRayId); } } catch (SocketTimeoutException e) { FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToRequestTimeout(httpOptions.getConnectTimeoutMillis(), httpOptions.getReadTimeoutMillis(), cfRayId); logger.error(1102, message, e); - fetchResponse = FetchResponse.failed(message, false, cfRayId); + fetchResponse = FetchResponse.failed(message, RefreshErrorCode.HTTP_REQUEST_TIMEOUT, e, false, cfRayId); } catch (Exception e) { FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId); logger.error(1103, message, e); - fetchResponse = FetchResponse.failed(message + " " + e.getMessage(), false, cfRayId); + fetchResponse = FetchResponse.failed(message, RefreshErrorCode.HTTP_REQUEST_FAILURE, e, false, cfRayId); } finally { if(fetchResponse == null) { - fetchResponse = FetchResponse.failed(ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId), false, cfRayId); + fetchResponse = FetchResponse.failed(ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId), RefreshErrorCode.UNEXPECTED_ERROR, null, false, cfRayId); } result.complete(fetchResponse); if (urlConnection != null) { @@ -271,13 +279,13 @@ private String readBody(InputStream inputStream) throws IOException { return body.toString(); } - private Result deserializeConfig(String json, String cfRayId) { + private Result deserializeConfig(String json, String cfRayId) { try { - return Result.success(Utils.deserializeConfig(json)); + return Result.success(Utils.deserializeConfig(json), RefreshErrorCode.NONE); } catch (Exception e) { FormattableLogMessage message = ConfigCatLogMessages.getFetchReceived200WithInvalidBodyError(cfRayId); this.logger.error(1105, message, e); - return Result.error(message, null); + return Result.error(message, null, RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, e); } } } diff --git a/src/main/java/com/configcat/ConfigService.java b/src/main/java/com/configcat/ConfigService.java index 6b0808e..e570c95 100644 --- a/src/main/java/com/configcat/ConfigService.java +++ b/src/main/java/com/configcat/ConfigService.java @@ -42,7 +42,7 @@ class ConfigService implements Closeable { private ScheduledExecutorService pollScheduler; private String cachedEntryString = ""; private Entry cachedEntry = Entry.EMPTY; - private CompletableFuture> runningTask; + private CompletableFuture> runningTask; private final AtomicBoolean initialized = new AtomicBoolean(false); private final AtomicBoolean userIndicatedOffline; private final AtomicBoolean inForegroundAndHasNetwork; @@ -96,7 +96,7 @@ public ConfigService(String sdkKey, hooks.invokeOnClientReady(determineCacheState()); FormattableLogMessage message = ConfigCatLogMessages.getAutoPollMaxInitWaitTimeReached(autoPollingMode.getMaxInitWaitTimeSeconds()); logger.warn(4200, message); - completeRunningTask(Result.error(message, cachedEntry)); + completeRunningTask(Result.error(message, cachedEntry, RefreshErrorCode.CLIENT_INIT_TIMED_OUT, null)); } } finally { lock.unlock(); @@ -133,11 +133,15 @@ public CompletableFuture refresh() { if (isOffline()) { String offlineWarning = ConfigCatLogMessages.CONFIG_SERVICE_CANNOT_INITIATE_HTTP_CALLS_WARN; logger.warn(3200, offlineWarning); - return CompletableFuture.completedFuture(new RefreshResult(false, offlineWarning)); + return CompletableFuture.completedFuture(new RefreshResult(false, offlineWarning, RefreshErrorCode.OFFLINE_CLIENT, null)); } return fetchIfOlder(Constants.DISTANT_FUTURE, false) - .thenApply(entryResult -> new RefreshResult(entryResult.error() == null, entryResult.error())); + .thenApply(entryResult -> { + boolean isSucceed = entryResult.error() == null; + return new RefreshResult(isSucceed, entryResult.error(), + isSucceed ? RefreshErrorCode.NONE : entryResult.errorCode(), entryResult.errorException()); + }); } public void setOnline() { @@ -167,7 +171,7 @@ private void switchStateIfNeeded() { } } - private CompletableFuture> fetchIfOlder(long threshold, boolean preferCached) { + private CompletableFuture> fetchIfOlder(long threshold, boolean preferCached) { lock.lock(); try { Entry fromCache = readCache(); @@ -179,12 +183,12 @@ private CompletableFuture> fetchIfOlder(long threshold, boolean pr // Cache isn't expired if (!cachedEntry.isExpired(threshold)) { setInitialized(); - return CompletableFuture.completedFuture(Result.success(cachedEntry)); + return CompletableFuture.completedFuture(Result.success(cachedEntry, RefreshErrorCode.NONE)); } // If we are in offline mode or the caller prefers cached values, do not initiate fetch. if (isOffline() || preferCached) { setInitialized(); - return CompletableFuture.completedFuture(Result.success(cachedEntry)); + return CompletableFuture.completedFuture(Result.success(cachedEntry, RefreshErrorCode.NONE)); } if (runningTask == null) { @@ -208,7 +212,7 @@ private void processResponse(FetchResponse response) { Entry entry = response.entry(); cachedEntry = entry; writeCache(entry); - completeRunningTask(Result.success(entry)); + completeRunningTask(Result.success(entry,RefreshErrorCode.NONE)); hooks.invokeOnConfigChanged(entry.getConfig().getEntries()); } else { if (response.isFetchTimeUpdatable()) { @@ -216,8 +220,8 @@ private void processResponse(FetchResponse response) { writeCache(cachedEntry); } completeRunningTask(response.isFailed() - ? Result.error(response.error(), cachedEntry) - : Result.success(cachedEntry)); + ? Result.error(response.error(), cachedEntry, response.errorCode(), response.errorException()) + : Result.success(cachedEntry, RefreshErrorCode.NONE)); } setInitialized(); } finally { @@ -225,7 +229,7 @@ private void processResponse(FetchResponse response) { } } - private void completeRunningTask(Result result) { + private void completeRunningTask(Result result) { runningTask.complete(result); runningTask = null; } diff --git a/src/main/java/com/configcat/EvaluationDetails.java b/src/main/java/com/configcat/EvaluationDetails.java index 3609e49..b6f1820 100644 --- a/src/main/java/com/configcat/EvaluationDetails.java +++ b/src/main/java/com/configcat/EvaluationDetails.java @@ -10,9 +10,11 @@ public class EvaluationDetails { private final User user; private final boolean isDefaultValue; private final Object error; + private final EvaluationErrorCode errorCode; private final long fetchTimeUnixMilliseconds; private final TargetingRule matchedTargetingRule; private final PercentageOption matchedPercentageOption; + private final Throwable errorException; public EvaluationDetails(T value, String key, @@ -20,6 +22,8 @@ public EvaluationDetails(T value, User user, boolean isDefaultValue, Object error, + EvaluationErrorCode errorCode, + Throwable errorException, long fetchTimeUnixMilliseconds, TargetingRule matchedTargetingRule, PercentageOption matchedPercentageOption) { @@ -29,17 +33,20 @@ public EvaluationDetails(T value, this.user = user; this.isDefaultValue = isDefaultValue; this.error = error; + this.errorCode = errorCode; + this.errorException = errorException; this.fetchTimeUnixMilliseconds = fetchTimeUnixMilliseconds; this.matchedTargetingRule = matchedTargetingRule; this.matchedPercentageOption = matchedPercentageOption; + } - static EvaluationDetails fromError(String key, T defaultValue, Object error, User user) { - return new EvaluationDetails<>(defaultValue, key, "", user, true, error, Constants.DISTANT_PAST, null, null); + static EvaluationDetails fromError(String key, T defaultValue, EvaluationErrorCode errorCode, Object error, Throwable errorException, User user) { + return new EvaluationDetails<>(defaultValue, key, "", user, true, error, errorCode, errorException, Constants.DISTANT_PAST, null, null); } EvaluationDetails asTypeSpecific() { - return new EvaluationDetails<>((TR) value, key, variationId, user, isDefaultValue, error, fetchTimeUnixMilliseconds, matchedTargetingRule, matchedPercentageOption); + return new EvaluationDetails<>((TR) value, key, variationId, user, isDefaultValue, error, errorCode, errorException, fetchTimeUnixMilliseconds, matchedTargetingRule, matchedPercentageOption); } /** @@ -87,6 +94,20 @@ public String getError() { return null; } + /** + * The code identifying the reason for the error in case the operation failed. (If the evaluation was successful, this will be EvaluationErrorCode.NONE.) + */ + public EvaluationErrorCode getErrorCode() { + return errorCode; + } + + /** + * The exception object related to the error in case the operation failed. (If the evaluation was successful, this will be null.) + */ + public Throwable getErrorException() { + return errorException; + } + /** * The last fetch time of the config.json in unix milliseconds format. */ diff --git a/src/main/java/com/configcat/EvaluationErrorCode.java b/src/main/java/com/configcat/EvaluationErrorCode.java new file mode 100644 index 0000000..69ab0d5 --- /dev/null +++ b/src/main/java/com/configcat/EvaluationErrorCode.java @@ -0,0 +1,55 @@ +package com.configcat; + +/** + * Specifies the possible evaluation error codes. + */ +public enum EvaluationErrorCode implements ErrorCode { + + /** An unexpected error occurred during the evaluation. */ + UNEXPECTED_ERROR(-1), + + /** No error occurred (the evaluation was successful). */ + NONE(0), + + /** + * The evaluation failed because of an error in the config model. + * (Most likely, invalid data was passed to the SDK via flag overrides.) + */ + INVALID_CONFIG_MODEL(1), + + /** + * The evaluation failed because of a type mismatch between the evaluated + * setting value and the specified default value. + */ + SETTING_VALUE_TYPE_MISMATCH(2), + + /** The evaluation failed because the config JSON was not available locally. */ + CONFIG_JSON_NOT_AVAILABLE(1000), + + /** + * The evaluation failed because the key of the evaluated setting was not found in + * the config JSON. + */ + SETTING_KEY_MISSING(1001); + + public final int code; + + EvaluationErrorCode(int code) { + this.code = code; + } + + @Override + public int code() { + return this.code; + } + + public static EvaluationErrorCode fromException(Throwable exception) { + if (exception instanceof InvalidConfigModelException) { + return INVALID_CONFIG_MODEL; + } + if (exception instanceof EvaluationException) { + return SETTING_VALUE_TYPE_MISMATCH; + } + return UNEXPECTED_ERROR; + } +} diff --git a/src/main/java/com/configcat/RefreshErrorCode.java b/src/main/java/com/configcat/RefreshErrorCode.java new file mode 100644 index 0000000..32d177e --- /dev/null +++ b/src/main/java/com/configcat/RefreshErrorCode.java @@ -0,0 +1,54 @@ +package com.configcat; + +/** + * Specifies the possible config data refresh error codes. + */ +public enum RefreshErrorCode implements ErrorCode { + + /** An unexpected error occurred during the refresh operation. */ + UNEXPECTED_ERROR(-1), + + /** No error occurred (the refresh operation was successful). */ + NONE(0), + + /** + * The refresh operation failed because the client is configured to use the `OverrideBehaviour.LOCAL_ONLY` + * override behavior, which prevents synchronization with the external cache and making HTTP requests. + */ + LOCAL_ONLY_CLIENT(1), + + /** The refresh operation failed because the client is in offline mode, it cannot initiate HTTP requests. */ + OFFLINE_CLIENT(3200), + + /** + * The refresh operation failed because a HTTP response indicating an + * invalid SDK Key was received (403 Forbidden or 404 Not Found). + */ + INVALID_SDK_KEY(1100), + + /** The refresh operation failed because an invalid HTTP response was received (unexpected HTTP status code). */ + UNEXPECTED_HTTP_RESPONSE(1101), + + /** The refresh operation failed because the HTTP request timed out. */ + HTTP_REQUEST_TIMEOUT(1102), + + /** The refresh operation failed because the HTTP request failed (most likely, due to a local network issue). */ + HTTP_REQUEST_FAILURE(1103), + + /** The refresh operation failed because an invalid HTTP response was received (200 OK with an invalid content). */ + INVALID_HTTP_RESPONSE_CONTENT(1105), + + /** Client initialization could not complete within `maxInitWaitTimeSeconds`. **/ + CLIENT_INIT_TIMED_OUT(4200); + + public final int code; + + RefreshErrorCode(int code) { + this.code = code; + } + + @Override + public int code() { + return this.code; + } +} diff --git a/src/main/java/com/configcat/RefreshResult.java b/src/main/java/com/configcat/RefreshResult.java index f382201..442c5a1 100644 --- a/src/main/java/com/configcat/RefreshResult.java +++ b/src/main/java/com/configcat/RefreshResult.java @@ -1,25 +1,48 @@ package com.configcat; +import java.util.Map; + /** * Represents the result of a forceRefresh() call. */ public class RefreshResult { private final boolean success; private final Object error; + private final RefreshErrorCode errorCode; + private final Throwable errorException; - RefreshResult(boolean success, Object error) { + RefreshResult(boolean success, Object error, RefreshErrorCode errorCode, Throwable errorException) { this.success = success; this.error = error; + this.errorCode = errorCode; + this.errorException = errorException; } public boolean isSuccess() { return success; } + /** + * Error message in case the operation failed, otherwise null. + */ public String error() { if(error != null) { return error.toString(); } return null; } + + /** + * The code identifying the reason for the error in case the operation failed. + */ + public RefreshErrorCode errorCode() { + return errorCode; + } + + /** + * The exception object related to the error in case the operation failed (if any). + */ + public Throwable errorException() { + return errorException; + } } diff --git a/src/main/java/com/configcat/RolloutEvaluator.java b/src/main/java/com/configcat/RolloutEvaluator.java index 43eb83e..aca1913 100644 --- a/src/main/java/com/configcat/RolloutEvaluator.java +++ b/src/main/java/com/configcat/RolloutEvaluator.java @@ -93,7 +93,7 @@ private EvaluationResult evaluateTargetingRules(Setting setting, EvaluationConte } if (rule.getPercentageOptions() == null || rule.getPercentageOptions().length == 0) { - throw new IllegalArgumentException("Targeting rule THEN part is missing or invalid."); + throw new InvalidConfigModelException( "Targeting rule THEN part is missing or invalid."); } evaluateLogger.increaseIndentLevel(); @@ -193,7 +193,7 @@ private boolean evaluateUserCondition(UserCondition userCondition, EvaluationCon } if (comparator == null) { - throw new IllegalArgumentException(COMPARISON_OPERATOR_IS_INVALID); + throw new InvalidConfigModelException( COMPARISON_OPERATOR_IS_INVALID); } switch (comparator) { case CONTAINS_ANY_OF: @@ -265,7 +265,7 @@ private boolean evaluateUserCondition(UserCondition userCondition, EvaluationCon String[] userAttributeAsStringArray = getUserAttributeAsStringArray(userCondition, context, comparisonAttribute, userAttributeValue); return evaluateArrayContains(userCondition, configSalt, contextSalt, userAttributeAsStringArray, negateArrayContains, hashedArrayContains); default: - throw new IllegalArgumentException(COMPARISON_OPERATOR_IS_INVALID); + throw new InvalidConfigModelException( COMPARISON_OPERATOR_IS_INVALID); } } @@ -358,6 +358,7 @@ private boolean evaluateArrayContains(UserCondition userCondition, String config if (userContainsValues.length == 0) { return false; } + for (String userContainsValue : userContainsValues) { String userContainsValueConverted = hashedArrayContains ? getSaltedUserValue(userContainsValue, ensureConfigSalt(configSalt), contextSalt) : userContainsValue; for (String inValuesElement : comparisonValues) { @@ -399,21 +400,21 @@ private boolean evaluateHashedStartOrEndsWith(UserCondition userCondition, Strin for (String comparisonValueHashedStartsEnds : comparisonValues) { int indexOf = ensureComparisonValue(comparisonValueHashedStartsEnds).indexOf("_"); if (indexOf <= 0) { - throw new IllegalArgumentException(COMPARISON_VALUE_IS_MISSING_OR_INVALID); + throw new InvalidConfigModelException( COMPARISON_VALUE_IS_MISSING_OR_INVALID); } String comparedTextLength = comparisonValueHashedStartsEnds.substring(0, indexOf).trim(); int comparedTextLengthInt; try { comparedTextLengthInt = Integer.parseInt(comparedTextLength); } catch (NumberFormatException e) { - throw new IllegalArgumentException(COMPARISON_VALUE_IS_MISSING_OR_INVALID); + throw new InvalidConfigModelException( COMPARISON_VALUE_IS_MISSING_OR_INVALID); } if (userAttributeValueUTF8.length < comparedTextLengthInt) { continue; } String comparisonHashValue = comparisonValueHashedStartsEnds.substring(indexOf + 1); if (comparisonHashValue.isEmpty()) { - throw new IllegalArgumentException(COMPARISON_VALUE_IS_MISSING_OR_INVALID); + throw new InvalidConfigModelException( COMPARISON_VALUE_IS_MISSING_OR_INVALID); } byte[] userValueSubStringByteArray; if (UserComparator.HASHED_STARTS_WITH.equals(comparator) || UserComparator.HASHED_NOT_STARTS_WITH.equals(comparator)) { @@ -550,11 +551,11 @@ private boolean evaluateSegmentCondition(SegmentCondition segmentCondition, Eval } if (segment == null) { - throw new IllegalArgumentException("Segment reference is invalid."); + throw new InvalidConfigModelException( "Segment reference is invalid."); } String segmentName = segment.getName(); if (segmentName == null || segmentName.isEmpty()) { - throw new IllegalArgumentException("Segment name is missing."); + throw new InvalidConfigModelException( "Segment name is missing."); } evaluateLogger.logSegmentEvaluationStart(segmentName); @@ -564,7 +565,7 @@ private boolean evaluateSegmentCondition(SegmentCondition segmentCondition, Eval SegmentComparator segmentComparator = SegmentComparator.fromId(segmentCondition.getSegmentComparator()); if (segmentComparator == null) { - throw new IllegalArgumentException("Segment comparison operator is invalid."); + throw new InvalidConfigModelException( "Segment comparison operator is invalid."); } switch (segmentComparator) { case IS_IN_SEGMENT: @@ -574,7 +575,7 @@ private boolean evaluateSegmentCondition(SegmentCondition segmentCondition, Eval result = !segmentRulesResult; break; default: - throw new IllegalArgumentException("Segment comparison operator is invalid."); + throw new InvalidConfigModelException( "Segment comparison operator is invalid."); } evaluateLogger.logSegmentEvaluationResult(segmentCondition, segment, result, segmentRulesResult); @@ -593,7 +594,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer String prerequisiteFlagKey = prerequisiteFlagCondition.getPrerequisiteFlagKey(); Setting prerequisiteFlagSetting = context.getSettings().get(prerequisiteFlagKey); if (prerequisiteFlagKey == null || prerequisiteFlagKey.isEmpty() || prerequisiteFlagSetting == null) { - throw new IllegalArgumentException("Prerequisite flag key is missing or invalid."); + throw new InvalidConfigModelException( "Prerequisite flag key is missing or invalid."); } SettingType settingType = prerequisiteFlagSetting .getType(); @@ -601,7 +602,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer (settingType == SettingType.STRING && prerequisiteFlagCondition.getValue().getStringValue() == null) || (settingType == SettingType.INT && prerequisiteFlagCondition.getValue().getIntegerValue() == null) || (settingType == SettingType.DOUBLE && prerequisiteFlagCondition.getValue().getDoubleValue() == null)) { - throw new IllegalArgumentException("Type mismatch between comparison value '" + prerequisiteFlagCondition.getValue() + "' and prerequisite flag '" + prerequisiteFlagKey + "'."); + throw new InvalidConfigModelException( "Type mismatch between comparison value '" + prerequisiteFlagCondition.getValue() + "' and prerequisite flag '" + prerequisiteFlagKey + "'."); } List visitedKeys = context.getVisitedKeys(); @@ -611,7 +612,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer visitedKeys.add(context.getKey()); if (visitedKeys.contains(prerequisiteFlagKey)) { String dependencyCycle = EvaluateLogger.formatCircularDependencyList(visitedKeys, prerequisiteFlagKey); - throw new IllegalArgumentException("Circular dependency detected between the following depending flags: " + dependencyCycle + "."); + throw new InvalidConfigModelException( "Circular dependency detected between the following depending flags: " + dependencyCycle + "."); } evaluateLogger.logPrerequisiteFlagEvaluationStart(prerequisiteFlagKey); @@ -633,7 +634,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer boolean result; if (prerequisiteComparator == null) { - throw new IllegalArgumentException("Prerequisite Flag comparison operator is invalid."); + throw new InvalidConfigModelException( "Prerequisite Flag comparison operator is invalid."); } switch (prerequisiteComparator) { case EQUALS: @@ -643,7 +644,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer result = !conditionValue.equalsBasedOnSettingType(evaluateResult.value, prerequisiteFlagSetting.getType()); break; default: - throw new IllegalArgumentException("Prerequisite Flag comparison operator is invalid."); + throw new InvalidConfigModelException( "Prerequisite Flag comparison operator is invalid."); } evaluateLogger.logPrerequisiteFlagEvaluationResult(prerequisiteFlagCondition, evaluateResult.value, result); @@ -695,19 +696,19 @@ private EvaluationResult evaluatePercentageOptions(PercentageOption[] percentage } } - throw new IllegalArgumentException("Sum of percentage option percentages is less than 100."); + throw new InvalidConfigModelException( "Sum of percentage option percentages is less than 100."); } private static T ensureComparisonValue(T value) { if (value == null) { - throw new IllegalArgumentException(COMPARISON_VALUE_IS_MISSING_OR_INVALID); + throw new InvalidConfigModelException( COMPARISON_VALUE_IS_MISSING_OR_INVALID); } return value; } private static String ensureConfigSalt(String configSalt){ if(configSalt == null){ - throw new IllegalArgumentException("Config JSON salt is missing."); + throw new InvalidConfigModelException( "Config JSON salt is missing."); } return configSalt; } @@ -717,7 +718,7 @@ private void validateSettingValueType(SettingValue settingValue, SettingType set || (SettingType.INT.equals(settingType) && settingValue.getIntegerValue() == null ) || (SettingType.DOUBLE.equals(settingType) && settingValue.getDoubleValue() == null) || (SettingType.BOOLEAN.equals(settingType) && settingValue.getBooleanValue() == null)) { - throw new IllegalArgumentException("Setting value is not of the expected type " + settingType.name() + "."); + throw new InvalidConfigModelException( "Setting value is not of the expected type " + settingType.name() + "."); } } } @@ -728,3 +729,9 @@ public RolloutEvaluatorException(String message) { } } +class InvalidConfigModelException extends IllegalArgumentException { + public InvalidConfigModelException(String message) { + super(message); + } +} + diff --git a/src/main/java/com/configcat/Utils.java b/src/main/java/com/configcat/Utils.java index a118b0a..035b22e 100644 --- a/src/main/java/com/configcat/Utils.java +++ b/src/main/java/com/configcat/Utils.java @@ -64,13 +64,24 @@ private Constants() { /* prevent from instantiation*/ } static final int SDK_KEY_SECTION_LENGTH = 22; } -final class Result { +/** + * Common interface for Result. + */ +interface ErrorCode { + int code(); +} + +final class Result { private final T value; private final Object error; + private final E errorCode; + private final Throwable errorException; - private Result(T value, Object error) { + private Result(T value, Object error, E errorCode, Throwable errorException) { this.value = value; this.error = error; + this.errorCode = errorCode; + this.errorException = errorException; } T value() { @@ -81,11 +92,22 @@ Object error() { return this.error; } - static Result error(Object error, T value) { - return new Result<>(value, error); + E errorCode() {return this.errorCode;} + + Throwable errorException() {return this.errorException;} + + static Result error(Object error, T value, E errorCode, Throwable errorException) { + return new Result<>(value, error, errorCode, errorException); + } + + static Result success(T value, E errorCode) { + return new Result<>(value, null, errorCode, null); } +} - static Result success(T value) { - return new Result<>(value, null); +final class EvaluationException extends IllegalArgumentException { + EvaluationException(String message) { + super(message); } } + diff --git a/src/test/java/com/configcat/ConfigCatClientTest.java b/src/test/java/com/configcat/ConfigCatClientTest.java index 1b596ce..7ca816f 100644 --- a/src/test/java/com/configcat/ConfigCatClientTest.java +++ b/src/test/java/com/configcat/ConfigCatClientTest.java @@ -1,6 +1,7 @@ package com.configcat; import java9.util.concurrent.CompletableFuture; +import kotlin.jvm.internal.Ref; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.Test; @@ -194,9 +195,12 @@ void getConfigurationReturnsPreviousCachedOnTimeout() throws IOException { server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); server.enqueue(new MockResponse().setResponseCode(200).setBody("delayed").setBodyDelay(3, TimeUnit.SECONDS)); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.NONE, result.errorCode()); assertEquals("fakeValue", cl.getValue(String.class, "fakeKey", null)); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, result.errorCode()); + assertEquals("Read timed out", result.errorException().getMessage()); assertEquals("fakeValue", cl.getValue(String.class, "fakeKey", null)); server.close(); @@ -237,9 +241,12 @@ void getConfigurationReturnsPreviousCachedOnFailAsync() throws IOException, Exec server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); server.enqueue(new MockResponse().setResponseCode(500)); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.NONE, result.errorCode()); assertEquals("fakeValue", cl.getValueAsync(String.class, "fakeKey", null).get()); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, result.errorCode()); + assertNull(result.errorException()); assertEquals("fakeValue", cl.getValueAsync(String.class, "fakeKey", null).get()); server.close(); @@ -262,10 +269,14 @@ void getValueReturnsDefaultOnExceptionRepeatedly() throws IOException { server.enqueue(new MockResponse().setResponseCode(200).setBody(badJson)); server.enqueue(new MockResponse().setResponseCode(200).setBody(badJson).setBodyDelay(3, TimeUnit.SECONDS)); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, result.errorCode()); + assertNotNull(result.errorException()); assertSame(def, cl.getValue(String.class, "test", def)); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, result.errorCode()); + assertEquals("Read timed out", result.errorException().getMessage()); assertSame(def, cl.getValue(String.class, "test", def)); server.shutdown(); @@ -285,7 +296,9 @@ void forceRefreshWithTimeout() throws IOException { server.enqueue(new MockResponse().setResponseCode(200).setBody("test").setBodyDelay(3, TimeUnit.SECONDS)); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, result.errorCode()); + assertEquals("Read timed out", result.errorException().getMessage()); server.shutdown(); cl.close(); @@ -358,7 +371,9 @@ void testAutoPollRefreshFail() throws IOException { options.baseUrl(server.url("/").toString()); }); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, result.errorCode()); + assertNull(result.errorException()); assertEquals("", cl.getValue(String.class, "fakeKey", "")); server.close(); @@ -377,7 +392,9 @@ void testLazyRefreshFail() throws IOException { options.baseUrl(server.url("/").toString()); }); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, result.errorCode()); + assertNull(result.errorException()); assertEquals("", cl.getValue(String.class, "fakeKey", "")); server.close(); @@ -396,7 +413,9 @@ void testManualPollRefreshFail() throws IOException { options.baseUrl(server.url("/").toString()); }); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, result.errorCode()); + assertNull(result.errorException()); assertEquals("", cl.getValue(String.class, "fakeKey", "")); server.close(); @@ -478,19 +497,25 @@ void testOnlineOffline() throws IOException { assertFalse(cl.isOffline()); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.NONE, result.errorCode()); + assertNull(result.errorException()); assertEquals(1, server.getRequestCount()); cl.setOffline(); assertTrue(cl.isOffline()); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.OFFLINE_CLIENT, result.errorCode()); + assertNull(result.errorException()); assertEquals(1, server.getRequestCount()); cl.setOnline(); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.NONE, result.errorCode()); + assertNull(result.errorException()); assertEquals(2, server.getRequestCount()); @@ -513,7 +538,10 @@ void testInitOffline() throws IOException { assertTrue(cl.isOffline()); - cl.forceRefresh(); + RefreshResult refreshResult = cl.forceRefresh(); + assertFalse(refreshResult.isSuccess()); + assertEquals(RefreshErrorCode.OFFLINE_CLIENT, refreshResult.errorCode()); + assertEquals("Client is in offline mode, it cannot initiate HTTP calls.", refreshResult.error()); assertEquals(0, server.getRequestCount()); @@ -746,6 +774,8 @@ void testOnFlagEvaluationError() throws IOException { options.baseUrl(server.url("/").toString()); options.hooks().addOnFlagEvaluated(details -> { assertEquals("", details.getValue()); + assertEquals(EvaluationErrorCode.CONFIG_JSON_NOT_AVAILABLE, details.getErrorCode()); + assertNull(details.getErrorException()); assertEquals("Config JSON is not present when evaluating setting 'key'. Returning the `defaultValue` parameter that you specified in your application: ''.", details.getError()); assertTrue(details.isDefaultValue()); called.set(true); @@ -822,6 +852,7 @@ void getAllValueDetails() throws IOException { assertTrue((boolean) element.getValue()); assertFalse(element.isDefaultValue()); assertNull(element.getError()); + assertEquals(EvaluationErrorCode.NONE, element.getErrorCode()); assertEquals("fakeId1", element.getVariationId()); //assert result 2 @@ -830,6 +861,7 @@ void getAllValueDetails() throws IOException { assertFalse((boolean) element.getValue()); assertFalse(element.isDefaultValue()); assertNull(element.getError()); + assertEquals(EvaluationErrorCode.NONE, element.getErrorCode()); assertEquals("fakeId2", element.getVariationId()); server.shutdown(); cl.close(); @@ -859,6 +891,7 @@ void getAllValueDetailsAsync() throws IOException, ExecutionException, Interrupt assertTrue((boolean) element.getValue()); assertFalse(element.isDefaultValue()); assertNull(element.getError()); + assertEquals(EvaluationErrorCode.NONE, element.getErrorCode()); assertEquals("fakeId1", element.getVariationId()); //assert result 2 @@ -867,6 +900,7 @@ void getAllValueDetailsAsync() throws IOException, ExecutionException, Interrupt assertFalse((boolean) element.getValue()); assertFalse(element.isDefaultValue()); assertNull(element.getError()); + assertEquals(EvaluationErrorCode.NONE, element.getErrorCode()); assertEquals("fakeId2", element.getVariationId()); server.shutdown(); cl.close(); diff --git a/src/test/java/com/configcat/ConfigFetcherTest.java b/src/test/java/com/configcat/ConfigFetcherTest.java index c8cc04f..d638333 100644 --- a/src/test/java/com/configcat/ConfigFetcherTest.java +++ b/src/test/java/com/configcat/ConfigFetcherTest.java @@ -78,6 +78,9 @@ void fetchException() throws IOException, ExecutionException, InterruptedExcepti FetchResponse response = fetch.fetchAsync(null).get(); assertTrue(response.isFailed()); + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, response.errorCode()); + assertEquals("Request timed out while trying to fetch config JSON. Timeout values: [connect: 10000ms, read: 1000ms]", response.error().toString()); + assertEquals("Read timed out", response.errorException().getMessage()); assertEquals(Entry.EMPTY, response.entry()); fetch.close(); @@ -135,6 +138,8 @@ void fetchSuccess() throws Exception { FetchResponse response = fetcher.fetchAsync(null).get(); assertTrue(response.isFetched()); + assertNull(response.error()); + assertEquals(RefreshErrorCode.NONE, response.errorCode()); assertEquals("fakeValue", response.entry().getConfig().getEntries().get("fakeKey").getSettingsValue().getStringValue()); fetcher.close(); @@ -161,8 +166,12 @@ void fetchEmpty(String body) throws Exception { FetchResponse response = fetcher.fetchAsync(null).get(); assertFalse(response.isFetched()); + assertTrue(response.isFailed()); + assertEquals(RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, response.errorCode()); + assertNotNull(response.errorException()); assertEquals("Fetching config JSON was successful but the HTTP response content was invalid.", response.error().toString()); + fetcher.close(); } @@ -200,6 +209,8 @@ void fetchedFail403ContainsCFRAY() throws Exception { FetchResponse response = fetcher.fetchAsync("fakeETag").get(); assertTrue(response.isFailed()); + assertEquals(RefreshErrorCode.INVALID_SDK_KEY, response.errorCode()); + assertNull(response.errorException()); assertTrue(response.error().toString().contains("(Ray ID: 12345)")); verify(mockLogger, times(1)).error(anyString(), eq(1100), eq(ConfigCatLogMessages.getFetchFailedDueToInvalidSDKKey("12345"))); @@ -226,7 +237,8 @@ void fetchedNotModified304ContainsCFRAY() throws Exception { FetchResponse response = fetcher.fetchAsync("fakeETag").get(); assertTrue(response.isNotModified()); - + assertNull(response.error()); + assertEquals(RefreshErrorCode.NONE, response.errorCode()); verify(mockLogger, times(1)).debug(anyString(), eq(0), eq(String.format("Fetch was successful: config not modified. %s", ConfigCatLogMessages.getCFRayIdPostFix("12345")))); fetcher.close(); @@ -252,7 +264,8 @@ void fetchedReceivedInvalidBodyContainsCFRAY() throws Exception { assertTrue(response.isFailed()); assertTrue(response.error().toString().contains("(Ray ID: 12345)")); - + assertEquals(RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, response.errorCode()); + assertNotNull(response.errorException()); verify(mockLogger, times(1)).error(anyString(), eq(1105), eq(ConfigCatLogMessages.getFetchReceived200WithInvalidBodyError("12345")), any(), any(Exception.class)); fetcher.close(); @@ -280,7 +293,8 @@ void fetchFailedDueToRequestTimeoutContainsCFRAY() throws Exception { assertTrue(response.isFailed()); assertTrue(response.error().toString().contains("Request timed out while trying to fetch config JSON.")); assertTrue(response.error().toString().contains("(Ray ID: timeout-ray-123)")); - + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, response.errorCode()); + assertEquals("Read timed out", response.errorException().getMessage()); verify(mockLogger, times(1)).error(anyString(), eq(1102), eq(ConfigCatLogMessages.getFetchFailedDueToRequestTimeout(10000, 1000, "timeout-ray-123")), any(), any(Exception.class)); fetcher.close(); @@ -312,6 +326,8 @@ void fetchFailedDueToUnexpectedErrorContainsCFRAY() throws Exception { assertTrue(response.isFailed()); assertTrue(response.error().toString().contains("Unexpected error occurred while trying to fetch config JSON.")); assertTrue(response.error().toString().contains("(Ray ID: unexpected-ray-456)")); + assertEquals(RefreshErrorCode.HTTP_REQUEST_FAILURE, response.errorCode()); + assertEquals("Premature EOF", response.errorException().getMessage()); fetcher.close(); } diff --git a/src/test/java/com/configcat/ConfigV2EvaluationTest.java b/src/test/java/com/configcat/ConfigV2EvaluationTest.java index 63f59e6..d7500ce 100644 --- a/src/test/java/com/configcat/ConfigV2EvaluationTest.java +++ b/src/test/java/com/configcat/ConfigV2EvaluationTest.java @@ -154,7 +154,7 @@ public void prerequisiteFlagCircularDependencyTest(String key, String dependency }); EvaluationDetails result = client.getValueDetails(String.class, key, null, null); - assertEquals("java.lang.IllegalArgumentException: Circular dependency detected between the following depending flags: " + dependencyCycle + ".", result.getError()); + assertEquals("Circular dependency detected between the following depending flags: " + dependencyCycle + ".", result.getError()); client.close(); } diff --git a/src/test/java/com/configcat/VariationIdTests.java b/src/test/java/com/configcat/VariationIdTests.java index 0b2df91..4b0798c 100644 --- a/src/test/java/com/configcat/VariationIdTests.java +++ b/src/test/java/com/configcat/VariationIdTests.java @@ -42,6 +42,7 @@ void getVariationIdWorks() { server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); EvaluationDetails valueDetails = client.getValueDetails(Boolean.class, "key1", null); assertEquals("fakeId1", valueDetails.getVariationId()); + assertEquals(EvaluationErrorCode.NONE, valueDetails.getErrorCode()); } @Test @@ -49,6 +50,7 @@ void getVariationIdAsyncWorks() throws ExecutionException, InterruptedException server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); EvaluationDetails valueDetails = client.getValueDetailsAsync(Boolean.class, "key2", null).get(); assertEquals("fakeId2", valueDetails.getVariationId()); + assertEquals(EvaluationErrorCode.NONE, valueDetails.getErrorCode()); } @Test @@ -56,6 +58,7 @@ void getVariationIdNotFound() { server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); EvaluationDetails valueDetails = client.getValueDetails(Boolean.class, "nonexisting", false); assertEquals("", valueDetails.getVariationId()); + assertEquals(EvaluationErrorCode.SETTING_KEY_MISSING, valueDetails.getErrorCode()); } @Test @@ -65,8 +68,11 @@ void getAllVariationIdsWorks() { List> allValueDetails = client.getAllValueDetails(null); assertEquals(3, allValueDetails.size()); assertEquals("fakeId1", allValueDetails.get(0).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(0).getErrorCode()); assertEquals("fakeId2", allValueDetails.get(1).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(1).getErrorCode()); assertEquals("fakeId3", allValueDetails.get(2).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(2).getErrorCode()); } @@ -85,8 +91,11 @@ void getAllVariationIdsAsyncWorks() throws ExecutionException, InterruptedExcept List> allValueDetails = client.getAllValueDetailsAsync(null).get(); assertEquals(3, allValueDetails.size()); assertEquals("fakeId1", allValueDetails.get(0).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(0).getErrorCode()); assertEquals("fakeId2", allValueDetails.get(1).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(1).getErrorCode()); assertEquals("fakeId3", allValueDetails.get(2).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(2).getErrorCode()); }