Skip to content

Commit 995469c

Browse files
Merge pull request #212 from Azure/zhiyuanliang/centralize-error-message (#213)
Centralize error message
2 parents 49d376b + 80a751f commit 995469c

File tree

9 files changed

+72
-37
lines changed

9 files changed

+72
-37
lines changed

src/appConfigurationImpl.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ import { AIConfigurationTracingOptions } from "./requestTracing/aiConfigurationT
6464
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
6565
import { ConfigurationClientManager } from "./configurationClientManager.js";
6666
import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js";
67-
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/error.js";
67+
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/errors.js";
68+
import { ErrorMessages } from "./common/errorMessages.js";
6869

6970
const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds
7071

@@ -159,10 +160,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
159160
} else {
160161
for (const setting of watchedSettings) {
161162
if (setting.key.includes("*") || setting.key.includes(",")) {
162-
throw new ArgumentError("The characters '*' and ',' are not supported in key of watched settings.");
163+
throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_KEY);
163164
}
164165
if (setting.label?.includes("*") || setting.label?.includes(",")) {
165-
throw new ArgumentError("The characters '*' and ',' are not supported in label of watched settings.");
166+
throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_LABEL);
166167
}
167168
this.#sentinels.push(setting);
168169
}
@@ -171,7 +172,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
171172
// custom refresh interval
172173
if (refreshIntervalInMs !== undefined) {
173174
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
174-
throw new RangeError(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
175+
throw new RangeError(ErrorMessages.INVALID_REFRESH_INTERVAL);
175176
}
176177
this.#kvRefreshInterval = refreshIntervalInMs;
177178
}
@@ -190,7 +191,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
190191
// custom refresh interval
191192
if (refreshIntervalInMs !== undefined) {
192193
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
193-
throw new RangeError(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
194+
throw new RangeError(ErrorMessages.INVALID_FEATURE_FLAG_REFRESH_INTERVAL);
194195
}
195196
this.#ffRefreshInterval = refreshIntervalInMs;
196197
}
@@ -203,7 +204,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
203204
const { secretRefreshIntervalInMs } = options.keyVaultOptions;
204205
if (secretRefreshIntervalInMs !== undefined) {
205206
if (secretRefreshIntervalInMs < MIN_SECRET_REFRESH_INTERVAL_IN_MS) {
206-
throw new RangeError(`The Key Vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`);
207+
throw new RangeError(ErrorMessages.INVALID_SECRET_REFRESH_INTERVAL);
207208
}
208209
this.#secretRefreshEnabled = true;
209210
this.#secretRefreshTimer = new RefreshTimer(secretRefreshIntervalInMs);
@@ -280,7 +281,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
280281
new Promise((_, reject) => {
281282
timeoutId = setTimeout(() => {
282283
abortController.abort(); // abort the initialization promise
283-
reject(new Error("Load operation timed out."));
284+
reject(new Error(ErrorMessages.LOAD_OPERATION_TIMEOUT));
284285
},
285286
startupTimeout);
286287
})
@@ -295,7 +296,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
295296
await new Promise(resolve => setTimeout(resolve, MIN_DELAY_FOR_UNHANDLED_FAILURE - timeElapsed));
296297
}
297298
}
298-
throw new Error("Failed to load.", { cause: error });
299+
throw new Error(ErrorMessages.LOAD_OPERATION_FAILED, { cause: error });
299300
} finally {
300301
clearTimeout(timeoutId); // cancel the timeout promise
301302
}
@@ -349,7 +350,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
349350
*/
350351
async refresh(): Promise<void> {
351352
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) {
352-
throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets.");
353+
throw new InvalidOperationError(ErrorMessages.REFRESH_NOT_ENABLED);
353354
}
354355

355356
if (this.#refreshInProgress) {
@@ -368,7 +369,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
368369
*/
369370
onRefresh(listener: () => any, thisArg?: any): Disposable {
370371
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) {
371-
throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets.");
372+
throw new InvalidOperationError(ErrorMessages.REFRESH_NOT_ENABLED);
372373
}
373374

374375
const boundedListener = listener.bind(thisArg);
@@ -848,7 +849,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
848849
}
849850

850851
this.#clientManager.refreshClients();
851-
throw new Error("All fallback clients failed to get configuration settings.");
852+
throw new Error(ErrorMessages.ALL_FALLBACK_CLIENTS_FAILED);
852853
}
853854

854855
async #resolveSecretReferences(secretReferences: ConfigurationSetting[], resultHandler: (key: string, value: unknown) => void): Promise<void> {
@@ -924,7 +925,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
924925
async #parseFeatureFlag(setting: ConfigurationSetting<string>): Promise<any> {
925926
const rawFlag = setting.value;
926927
if (rawFlag === undefined) {
927-
throw new ArgumentError("The value of configuration setting cannot be undefined.");
928+
throw new ArgumentError(ErrorMessages.CONFIGURATION_SETTING_VALUE_UNDEFINED);
928929
}
929930
const featureFlag = JSON.parse(rawFlag);
930931

@@ -1106,17 +1107,17 @@ function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector
11061107
const selector = { ...selectorCandidate };
11071108
if (selector.snapshotName) {
11081109
if (selector.keyFilter || selector.labelFilter || selector.tagFilters) {
1109-
throw new ArgumentError("Key, label or tag filters should not be specified while selecting a snapshot.");
1110+
throw new ArgumentError(ErrorMessages.INVALID_SNAPSHOT_SELECTOR);
11101111
}
11111112
} else {
11121113
if (!selector.keyFilter) {
1113-
throw new ArgumentError("Key filter cannot be null or empty.");
1114+
throw new ArgumentError(ErrorMessages.INVALID_KEY_FILTER);
11141115
}
11151116
if (!selector.labelFilter) {
11161117
selector.labelFilter = LabelFilter.Null;
11171118
}
11181119
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
1119-
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
1120+
throw new ArgumentError(ErrorMessages.INVALID_LABEL_FILTER);
11201121
}
11211122
if (selector.tagFilters) {
11221123
validateTagFilters(selector.tagFilters);
@@ -1168,7 +1169,7 @@ function validateTagFilters(tagFilters: string[]): void {
11681169
for (const tagFilter of tagFilters) {
11691170
const res = tagFilter.split("=");
11701171
if (res[0] === "" || res.length !== 2) {
1171-
throw new Error(`Invalid tag filter: ${tagFilter}. Tag filter must follow the format "tagName=tagValue".`);
1172+
throw new Error(`Invalid tag filter: ${tagFilter}. ${ErrorMessages.INVALID_TAG_FILTER}.`);
11721173
}
11731174
}
11741175
}

src/common/errorMessages.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { MIN_REFRESH_INTERVAL_IN_MS } from "../refresh/refreshOptions.js";
5+
import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "../keyvault/keyVaultOptions.js";
6+
7+
export const enum ErrorMessages {
8+
INVALID_WATCHED_SETTINGS_KEY = "The characters '*' and ',' are not supported in key of watched settings.",
9+
INVALID_WATCHED_SETTINGS_LABEL = "The characters '*' and ',' are not supported in label of watched settings.",
10+
INVALID_REFRESH_INTERVAL = `The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`,
11+
INVALID_FEATURE_FLAG_REFRESH_INTERVAL = `The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`,
12+
INVALID_SECRET_REFRESH_INTERVAL = `The Key Vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`,
13+
LOAD_OPERATION_TIMEOUT = "The load operation timed out.",
14+
LOAD_OPERATION_FAILED = "The load operation failed.",
15+
REFRESH_NOT_ENABLED = "Refresh is not enabled for key-values, feature flags or Key Vault secrets.",
16+
ALL_FALLBACK_CLIENTS_FAILED = "All fallback clients failed to get configuration settings.",
17+
CONFIGURATION_SETTING_VALUE_UNDEFINED = "The value of configuration setting cannot be undefined.",
18+
INVALID_SNAPSHOT_SELECTOR = "Key, label or tag filters should not be specified while selecting a snapshot.",
19+
INVALID_KEY_FILTER = "Key filter cannot be null or empty.",
20+
INVALID_LABEL_FILTER = "The characters '*' and ',' are not supported in label filters.",
21+
INVALID_TAG_FILTER = "Tag filter must follow the format 'tagName=tagValue'",
22+
CONNECTION_STRING_OR_ENDPOINT_MISSED = "A connection string or an endpoint with credential must be specified to create a client.",
23+
}
24+
25+
export const enum KeyVaultReferenceErrorMessages {
26+
KEY_VAULT_OPTIONS_UNDEFINED = "Failed to process the Key Vault reference because Key Vault options are not configured.",
27+
KEY_VAULT_REFERENCE_UNRESOLVABLE = "Failed to resolve the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret."
28+
}
File renamed without changes.

src/configurationClientManager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js";
88
import { isBrowser, isWebWorker } from "./requestTracing/utils.js";
99
import * as RequestTracing from "./requestTracing/constants.js";
1010
import { shuffleList, instanceOfTokenCredential } from "./common/utils.js";
11-
import { ArgumentError } from "./common/error.js";
11+
import { ArgumentError } from "./common/errors.js";
12+
import { ErrorMessages } from "./common/errorMessages.js";
1213

1314
// Configuration client retry options
1415
const CLIENT_MAX_RETRIES = 2;
@@ -80,7 +81,7 @@ export class ConfigurationClientManager {
8081
this.#credential = credential;
8182
staticClient = new AppConfigurationClient(this.endpoint.origin, this.#credential, this.#clientOptions);
8283
} else {
83-
throw new ArgumentError("A connection string or an endpoint with credential must be specified to create a client.");
84+
throw new ArgumentError(ErrorMessages.CONNECTION_STRING_OR_ENDPOINT_MISSED);
8485
}
8586

8687
this.#staticClients = [new ConfigurationClientWrapper(this.endpoint.origin, staticClient)];

src/keyvault/keyVaultKeyValueAdapter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { IKeyValueAdapter } from "../keyValueAdapter.js";
66
import { AzureKeyVaultSecretProvider } from "./keyVaultSecretProvider.js";
77
import { KeyVaultOptions } from "./keyVaultOptions.js";
88
import { RefreshTimer } from "../refresh/refreshTimer.js";
9-
import { ArgumentError, KeyVaultReferenceError } from "../common/error.js";
9+
import { ArgumentError, KeyVaultReferenceError } from "../common/errors.js";
10+
import { KeyVaultReferenceErrorMessages } from "../common/errorMessages.js";
1011
import { KeyVaultSecretIdentifier, parseKeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
1112
import { isRestError } from "@azure/core-rest-pipeline";
1213
import { AuthenticationError } from "@azure/identity";
@@ -26,7 +27,7 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
2627

2728
async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
2829
if (!this.#keyVaultOptions) {
29-
throw new ArgumentError("Failed to process the Key Vault reference because Key Vault options are not configured.");
30+
throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED);
3031
}
3132
let secretIdentifier: KeyVaultSecretIdentifier;
3233
try {

src/keyvault/keyVaultSecretProvider.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
import { KeyVaultOptions } from "./keyVaultOptions.js";
55
import { RefreshTimer } from "../refresh/refreshTimer.js";
6-
import { ArgumentError } from "../common/error.js";
6+
import { ArgumentError } from "../common/errors.js";
77
import { SecretClient, KeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
8+
import { KeyVaultReferenceErrorMessages } from "../common/errorMessages.js";
89

910
export class AzureKeyVaultSecretProvider {
1011
#keyVaultOptions: KeyVaultOptions | undefined;
@@ -51,7 +52,7 @@ export class AzureKeyVaultSecretProvider {
5152

5253
async #getSecretValueFromKeyVault(secretIdentifier: KeyVaultSecretIdentifier): Promise<unknown> {
5354
if (!this.#keyVaultOptions) {
54-
throw new ArgumentError("Failed to get secret value. The keyVaultOptions is not configured.");
55+
throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED);
5556
}
5657
const { name: secretName, vaultUrl, sourceId, version } = secretIdentifier;
5758
// precedence: secret clients > custom secret resolver
@@ -64,7 +65,7 @@ export class AzureKeyVaultSecretProvider {
6465
return await this.#keyVaultOptions.secretResolver(new URL(sourceId));
6566
}
6667
// When code reaches here, it means that the key vault reference cannot be resolved in all possible ways.
67-
throw new ArgumentError("Failed to process the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret.");
68+
throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_REFERENCE_UNRESOLVABLE);
6869
}
6970

7071
#getSecretClient(vaultUrl: URL): SecretClient | undefined {

test/keyvault.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const expect = chai.expect;
88
import { load } from "./exportedApi.js";
99
import { MAX_TIME_OUT, sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference, sleepInMs } from "./utils/testHelper.js";
1010
import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets";
11+
import { ErrorMessages, KeyVaultReferenceErrorMessages } from "../src/common/errorMessages.js";
1112

1213
const mockedData = [
1314
// key, secretUri, value
@@ -43,8 +44,8 @@ describe("key vault reference", function () {
4344
try {
4445
await load(createMockedConnectionString());
4546
} catch (error) {
46-
expect(error.message).eq("Failed to load.");
47-
expect(error.cause.message).eq("Failed to process the Key Vault reference because Key Vault options are not configured.");
47+
expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED);
48+
expect(error.cause.message).eq(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED);
4849
return;
4950
}
5051
// we should never reach here, load should throw an error
@@ -106,8 +107,8 @@ describe("key vault reference", function () {
106107
}
107108
});
108109
} catch (error) {
109-
expect(error.message).eq("Failed to load.");
110-
expect(error.cause.message).eq("Failed to process the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret.");
110+
expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED);
111+
expect(error.cause.message).eq(KeyVaultReferenceErrorMessages.KEY_VAULT_REFERENCE_UNRESOLVABLE);
111112
return;
112113
}
113114
// we should never reach here, load should throw an error
@@ -167,7 +168,7 @@ describe("key vault secret refresh", function () {
167168
secretRefreshIntervalInMs: 59999 // less than 60_000 milliseconds
168169
}
169170
});
170-
return expect(loadWithInvalidSecretRefreshInterval).eventually.rejectedWith("The Key Vault secret refresh interval cannot be less than 60000 milliseconds.");
171+
return expect(loadWithInvalidSecretRefreshInterval).eventually.rejectedWith(ErrorMessages.INVALID_SECRET_REFRESH_INTERVAL);
171172
});
172173

173174
it("should reload key vault secret when there is no change to key-values", async () => {

test/load.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ chai.use(chaiAsPromised);
77
const expect = chai.expect;
88
import { load } from "./exportedApi.js";
99
import { MAX_TIME_OUT, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetSnapshot, mockAppConfigurationClientListConfigurationSettingsForSnapshot, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper.js";
10+
import { ErrorMessages } from "../src/common/errorMessages.js";
1011

1112
const mockedKVs = [{
1213
key: "app.settings.fontColor",
@@ -164,7 +165,7 @@ describe("load", function () {
164165
snapshotName: "Test",
165166
labelFilter: "\0"
166167
}]
167-
})).eventually.rejectedWith("Key, label or tag filters should not be specified while selecting a snapshot.");
168+
})).eventually.rejectedWith(ErrorMessages.INVALID_SNAPSHOT_SELECTOR);
168169
});
169170

170171
it("should not include feature flags directly in the settings", async () => {
@@ -359,7 +360,7 @@ describe("load", function () {
359360
labelFilter: "*"
360361
}]
361362
});
362-
return expect(loadWithWildcardLabelFilter).to.eventually.rejectedWith("The characters '*' and ',' are not supported in label filters.");
363+
return expect(loadWithWildcardLabelFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_LABEL_FILTER);
363364
});
364365

365366
it("should not support , in label filters", async () => {
@@ -370,7 +371,7 @@ describe("load", function () {
370371
labelFilter: "labelA,labelB"
371372
}]
372373
});
373-
return expect(loadWithMultipleLabelFilter).to.eventually.rejectedWith("The characters '*' and ',' are not supported in label filters.");
374+
return expect(loadWithMultipleLabelFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_LABEL_FILTER);
374375
});
375376

376377
it("should throw exception when there is any invalid tag filter", async () => {
@@ -381,7 +382,7 @@ describe("load", function () {
381382
tagFilters: ["emptyTag"]
382383
}]
383384
});
384-
return expect(loadWithInvalidTagFilter).to.eventually.rejectedWith("Tag filter must follow the format \"tagName=tagValue\"");
385+
return expect(loadWithInvalidTagFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_TAG_FILTER);
385386
});
386387

387388
it("should throw exception when too many tag filters are provided", async () => {
@@ -404,7 +405,7 @@ describe("load", function () {
404405
}]
405406
});
406407
} catch (error) {
407-
expect(error.message).eq("Failed to load.");
408+
expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED);
408409
expect(error.cause.message).eq("Invalid request parameter 'tags'. Maximum number of tag filters is 5.");
409410
return;
410411
}

0 commit comments

Comments
 (0)