Skip to content

Commit f9c6749

Browse files
Merge pull request #212 from Azure/zhiyuanliang/centralize-error-message
Centralize error message
1 parent 6ceb126 commit f9c6749

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
@@ -56,7 +56,8 @@ import { AIConfigurationTracingOptions } from "./requestTracing/aiConfigurationT
5656
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
5757
import { ConfigurationClientManager } from "./configurationClientManager.js";
5858
import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js";
59-
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/error.js";
59+
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/errors.js";
60+
import { ErrorMessages } from "./common/errorMessages.js";
6061

6162
const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds
6263

@@ -151,10 +152,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
151152
} else {
152153
for (const setting of watchedSettings) {
153154
if (setting.key.includes("*") || setting.key.includes(",")) {
154-
throw new ArgumentError("The characters '*' and ',' are not supported in key of watched settings.");
155+
throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_KEY);
155156
}
156157
if (setting.label?.includes("*") || setting.label?.includes(",")) {
157-
throw new ArgumentError("The characters '*' and ',' are not supported in label of watched settings.");
158+
throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_LABEL);
158159
}
159160
this.#sentinels.push(setting);
160161
}
@@ -163,7 +164,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
163164
// custom refresh interval
164165
if (refreshIntervalInMs !== undefined) {
165166
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
166-
throw new RangeError(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
167+
throw new RangeError(ErrorMessages.INVALID_REFRESH_INTERVAL);
167168
}
168169
this.#kvRefreshInterval = refreshIntervalInMs;
169170
}
@@ -182,7 +183,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
182183
// custom refresh interval
183184
if (refreshIntervalInMs !== undefined) {
184185
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
185-
throw new RangeError(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
186+
throw new RangeError(ErrorMessages.INVALID_FEATURE_FLAG_REFRESH_INTERVAL);
186187
}
187188
this.#ffRefreshInterval = refreshIntervalInMs;
188189
}
@@ -195,7 +196,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
195196
const { secretRefreshIntervalInMs } = options.keyVaultOptions;
196197
if (secretRefreshIntervalInMs !== undefined) {
197198
if (secretRefreshIntervalInMs < MIN_SECRET_REFRESH_INTERVAL_IN_MS) {
198-
throw new RangeError(`The Key Vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`);
199+
throw new RangeError(ErrorMessages.INVALID_SECRET_REFRESH_INTERVAL);
199200
}
200201
this.#secretRefreshEnabled = true;
201202
this.#secretRefreshTimer = new RefreshTimer(secretRefreshIntervalInMs);
@@ -272,7 +273,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
272273
new Promise((_, reject) => {
273274
timeoutId = setTimeout(() => {
274275
abortController.abort(); // abort the initialization promise
275-
reject(new Error("Load operation timed out."));
276+
reject(new Error(ErrorMessages.LOAD_OPERATION_TIMEOUT));
276277
},
277278
startupTimeout);
278279
})
@@ -287,7 +288,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
287288
await new Promise(resolve => setTimeout(resolve, MIN_DELAY_FOR_UNHANDLED_FAILURE - timeElapsed));
288289
}
289290
}
290-
throw new Error("Failed to load.", { cause: error });
291+
throw new Error(ErrorMessages.LOAD_OPERATION_FAILED, { cause: error });
291292
} finally {
292293
clearTimeout(timeoutId); // cancel the timeout promise
293294
}
@@ -341,7 +342,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
341342
*/
342343
async refresh(): Promise<void> {
343344
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) {
344-
throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets.");
345+
throw new InvalidOperationError(ErrorMessages.REFRESH_NOT_ENABLED);
345346
}
346347

347348
if (this.#refreshInProgress) {
@@ -360,7 +361,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
360361
*/
361362
onRefresh(listener: () => any, thisArg?: any): Disposable {
362363
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) {
363-
throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets.");
364+
throw new InvalidOperationError(ErrorMessages.REFRESH_NOT_ENABLED);
364365
}
365366

366367
const boundedListener = listener.bind(thisArg);
@@ -840,7 +841,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
840841
}
841842

842843
this.#clientManager.refreshClients();
843-
throw new Error("All fallback clients failed to get configuration settings.");
844+
throw new Error(ErrorMessages.ALL_FALLBACK_CLIENTS_FAILED);
844845
}
845846

846847
async #resolveSecretReferences(secretReferences: ConfigurationSetting[], resultHandler: (key: string, value: unknown) => void): Promise<void> {
@@ -916,7 +917,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
916917
async #parseFeatureFlag(setting: ConfigurationSetting<string>): Promise<any> {
917918
const rawFlag = setting.value;
918919
if (rawFlag === undefined) {
919-
throw new ArgumentError("The value of configuration setting cannot be undefined.");
920+
throw new ArgumentError(ErrorMessages.CONFIGURATION_SETTING_VALUE_UNDEFINED);
920921
}
921922
const featureFlag = JSON.parse(rawFlag);
922923

@@ -983,17 +984,17 @@ function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector
983984
const selector = { ...selectorCandidate };
984985
if (selector.snapshotName) {
985986
if (selector.keyFilter || selector.labelFilter || selector.tagFilters) {
986-
throw new ArgumentError("Key, label or tag filters should not be specified while selecting a snapshot.");
987+
throw new ArgumentError(ErrorMessages.INVALID_SNAPSHOT_SELECTOR);
987988
}
988989
} else {
989990
if (!selector.keyFilter) {
990-
throw new ArgumentError("Key filter cannot be null or empty.");
991+
throw new ArgumentError(ErrorMessages.INVALID_KEY_FILTER);
991992
}
992993
if (!selector.labelFilter) {
993994
selector.labelFilter = LabelFilter.Null;
994995
}
995996
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
996-
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
997+
throw new ArgumentError(ErrorMessages.INVALID_LABEL_FILTER);
997998
}
998999
if (selector.tagFilters) {
9991000
validateTagFilters(selector.tagFilters);
@@ -1045,7 +1046,7 @@ function validateTagFilters(tagFilters: string[]): void {
10451046
for (const tagFilter of tagFilters) {
10461047
const res = tagFilter.split("=");
10471048
if (res[0] === "" || res.length !== 2) {
1048-
throw new Error(`Invalid tag filter: ${tagFilter}. Tag filter must follow the format "tagName=tagValue".`);
1049+
throw new Error(`Invalid tag filter: ${tagFilter}. ${ErrorMessages.INVALID_TAG_FILTER}.`);
10491050
}
10501051
}
10511052
}

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)