Skip to content

Commit 9e32db4

Browse files
update
1 parent a0e0792 commit 9e32db4

File tree

8 files changed

+77
-28
lines changed

8 files changed

+77
-28
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOp
4242
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
4343
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
4444
import { getFixedBackoffDuration, calculateBackoffDuration } from "./failover.js";
45-
import { OperationError, isFailoverableError, isRetriableError } from "./error.js";
45+
import { OperationError, ArgumentError, isFailoverableError, isRetriableError } from "./error.js";
4646

4747
type PagedSettingSelector = SettingSelector & {
4848
/**
@@ -126,10 +126,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
126126
} else {
127127
for (const setting of watchedSettings) {
128128
if (setting.key.includes("*") || setting.key.includes(",")) {
129-
throw new RangeError("The characters '*' and ',' are not supported in key of watched settings.");
129+
throw new ArgumentError("The characters '*' and ',' are not supported in key of watched settings.");
130130
}
131131
if (setting.label?.includes("*") || setting.label?.includes(",")) {
132-
throw new RangeError("The characters '*' and ',' are not supported in label of watched settings.");
132+
throw new ArgumentError("The characters '*' and ',' are not supported in label of watched settings.");
133133
}
134134
this.#sentinels.push(setting);
135135
}
@@ -138,7 +138,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
138138
// custom refresh interval
139139
if (refreshIntervalInMs !== undefined) {
140140
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
141-
throw new RangeError(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
141+
throw new ArgumentError(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
142142
} else {
143143
this.#kvRefreshInterval = refreshIntervalInMs;
144144
}
@@ -156,7 +156,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
156156
// custom refresh interval
157157
if (refreshIntervalInMs !== undefined) {
158158
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
159-
throw new RangeError(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
159+
throw new ArgumentError(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
160160
} else {
161161
this.#ffRefreshInterval = refreshIntervalInMs;
162162
}
@@ -265,7 +265,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
265265
const separator = options?.separator ?? ".";
266266
const validSeparators = [".", ",", ";", "-", "_", "__", "/", ":"];
267267
if (!validSeparators.includes(separator)) {
268-
throw new RangeError(`Invalid separator '${separator}'. Supported values: ${validSeparators.map(s => `'${s}'`).join(", ")}.`);
268+
throw new ArgumentError(`Invalid separator '${separator}'. Supported values: ${validSeparators.map(s => `'${s}'`).join(", ")}.`);
269269
}
270270

271271
// construct hierarchical data object from map
@@ -729,7 +729,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
729729
async #parseFeatureFlag(setting: ConfigurationSetting<string>): Promise<any> {
730730
const rawFlag = setting.value;
731731
if (rawFlag === undefined) {
732-
throw new RangeError("The value of configuration setting cannot be undefined.");
732+
throw new ArgumentError("The value of configuration setting cannot be undefined.");
733733
}
734734
const featureFlag = JSON.parse(rawFlag);
735735

@@ -953,13 +953,13 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
953953
return uniqueSelectors.map(selectorCandidate => {
954954
const selector = { ...selectorCandidate };
955955
if (!selector.keyFilter) {
956-
throw new RangeError("Key filter cannot be null or empty.");
956+
throw new ArgumentError("Key filter cannot be null or empty.");
957957
}
958958
if (!selector.labelFilter) {
959959
selector.labelFilter = LabelFilter.Null;
960960
}
961961
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
962-
throw new RangeError("The characters '*' and ',' are not supported in label filters.");
962+
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
963963
}
964964
return selector;
965965
});

src/ConfigurationClientManager.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { TokenCredential } from "@azure/identity";
77
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
88
import { isBrowser, isWebWorker } from "./requestTracing/utils.js";
99
import * as RequestTracing from "./requestTracing/constants.js";
10-
import { instanceOfTokenCredential, shuffleList, getValidUrl } from "./common/utils.js";
10+
import { instanceOfTokenCredential, shuffleList, getEndpointUrl } from "./common/utils.js";
11+
import { ArgumentError } from "./error.js";
1112

1213
// Configuration client retry options
1314
const CLIENT_MAX_RETRIES = 2;
@@ -60,18 +61,18 @@ export class ConfigurationClientManager {
6061
const regexMatch = connectionString.match(ConnectionStringRegex);
6162
if (regexMatch) {
6263
const endpointFromConnectionStr = regexMatch[1];
63-
this.endpoint = getValidUrl(endpointFromConnectionStr);
64+
this.endpoint = getEndpointUrl(endpointFromConnectionStr);
6465
this.#id = regexMatch[2];
6566
this.#secret = regexMatch[3];
6667
} else {
67-
throw new RangeError(`Invalid connection string. Valid connection strings should match the regex '${ConnectionStringRegex.source}'.`);
68+
throw new ArgumentError(`Invalid connection string. Valid connection strings should match the regex '${ConnectionStringRegex.source}'.`);
6869
}
6970
staticClient = new AppConfigurationClient(connectionString, this.#clientOptions);
7071
} else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && credentialPassed) {
7172
let endpoint = connectionStringOrEndpoint;
7273
// ensure string is a valid URL.
7374
if (typeof endpoint === "string") {
74-
endpoint = getValidUrl(endpoint);
75+
endpoint = getEndpointUrl(endpoint);
7576
}
7677

7778
const credential = credentialOrOptions as TokenCredential;
@@ -81,7 +82,7 @@ export class ConfigurationClientManager {
8182
this.#credential = credential;
8283
staticClient = new AppConfigurationClient(this.endpoint.origin, this.#credential, this.#clientOptions);
8384
} else {
84-
throw new RangeError("A connection string or an endpoint with credential must be specified to create a client.");
85+
throw new ArgumentError("A connection string or an endpoint with credential must be specified to create a client.");
8586
}
8687

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

src/common/utils.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,15 @@ export function shuffleList<T>(array: T[]): T[] {
3131
return array;
3232
}
3333

34-
export function getValidUrl(endpoint: string): URL {
34+
export function getEndpointUrl(endpoint: string): URL {
3535
try {
3636
return new URL(endpoint);
3737
} catch (error) {
38-
if (error.code === "ERR_INVALID_URL") {
39-
throw new RangeError("Invalid endpoint URL.", { cause: error });
40-
} else {
41-
throw error;
42-
}
38+
throw new TypeError(`Invalid Endpoint URL: ${endpoint}`);
4339
}
4440
}
4541

46-
export function getUrlHost(url: string) {
42+
export function getUrlHost(url: string): string {
4743
return new URL(url).host;
4844
}
4945

src/error.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ export class OperationError extends Error {
1414
}
1515
}
1616

17+
/**
18+
* Error thrown when an argument or configuration is invalid.
19+
*/
20+
export class ArgumentError extends Error {
21+
constructor(message: string) {
22+
super(message);
23+
this.name = "ArgumentError";
24+
}
25+
}
26+
1727
export function isFailoverableError(error: any): boolean {
1828
if (!isRestError(error)) {
1929
return false;
@@ -33,8 +43,10 @@ export function isFailoverableError(error: any): boolean {
3343

3444
export function isRetriableError(error: any): boolean {
3545
if (error instanceof AuthenticationError || // this error occurs when using wrong credential to access the key vault
36-
error instanceof RangeError || // this error is caused by misconfiguration of the Azure App Configuration provider
37-
error instanceof OperationError) {
46+
error instanceof ArgumentError || // this error is caused by misconfiguration of the Azure App Configuration provider
47+
error instanceof OperationError ||
48+
error instanceof TypeError ||
49+
error instanceof RangeError) {
3850
return false;
3951
}
4052
return true;

src/keyvault/AzureKeyVaultKeyValueAdapter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ConfigurationSetting, isSecretReference, parseSecretReference } from "@
55
import { IKeyValueAdapter } from "../IKeyValueAdapter.js";
66
import { KeyVaultOptions } from "./KeyVaultOptions.js";
77
import { getUrlHost } from "../common/utils.js";
8+
import { ArgumentError } from "../error.js";
89
import { SecretClient, parseKeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
910

1011
export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
@@ -25,7 +26,7 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
2526
async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
2627
// TODO: cache results to save requests.
2728
if (!this.#keyVaultOptions) {
28-
throw new RangeError("Failed to process the key vault reference. The keyVaultOptions is not configured.");
29+
throw new ArgumentError("Failed to process the key vault reference. The keyVaultOptions is not configured.");
2930
}
3031

3132
// precedence: secret clients > credential > secret resolver
@@ -46,7 +47,7 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
4647

4748
// When code reaches here, it means the key vault secret reference is not resolved.
4849

49-
throw new RangeError("Failed to process the key vault reference. No key vault credential or secret resolver callback is configured.");
50+
throw new ArgumentError("Failed to process the key vault reference. No key vault credential or secret resolver callback is configured.");
5051
}
5152

5253
/**

src/refresh/RefreshTimer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4+
import { ArgumentError } from "../error.js";
5+
46
export class RefreshTimer {
57
#backoffEnd: number; // Timestamp
68
#interval: number;
79

810
constructor(interval: number) {
911
if (interval <= 0) {
10-
throw new RangeError(`Refresh interval must be greater than 0. Given: ${this.#interval}`);
12+
throw new ArgumentError(`Refresh interval must be greater than 0. Given: ${this.#interval}`);
1113
}
1214

1315
this.#interval = interval;

test/load.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ describe("load", function () {
119119

120120
it("should throw error given invalid endpoint URL", async () => {
121121
const credential = createMockedTokenCredential();
122-
return expect(load("invalid-endpoint-url", credential)).eventually.rejectedWith("Invalid endpoint URL.");
122+
return expect(load("invalid-endpoint-url", credential)).eventually.rejectedWith("Invalid Endpoint URL: invalid-endpoint-url");
123123
});
124124

125125
it("should not include feature flags directly in the settings", async () => {

test/startup.test.ts

Lines changed: 38 additions & 1 deletion
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";
99
import { MAX_TIME_OUT, createMockedConnectionString, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js";
10+
import { AuthenticationError } from "@azure/identity";
1011

1112
describe("startup", function () {
1213
this.timeout(MAX_TIME_OUT);
@@ -51,7 +52,25 @@ describe("startup", function () {
5152
expect(attempt).eq(1);
5253
});
5354

54-
it("should not retry on non-retriable error", async () => {
55+
it("should not retry on non-retriable TypeError", async () => {
56+
let attempt = 0;
57+
const failForAllAttempts = () => {
58+
attempt += 1;
59+
throw new TypeError("Non-retriable Test Error");
60+
};
61+
mockAppConfigurationClientListConfigurationSettings(
62+
[[{key: "TestKey", value: "TestValue"}].map(createMockedKeyValue)],
63+
failForAllAttempts);
64+
65+
await expect(load(createMockedConnectionString(), {
66+
startupOptions: {
67+
timeoutInMs: 10_000
68+
}
69+
})).to.be.rejectedWith("Non-retriable Test Error");
70+
expect(attempt).eq(1);
71+
});
72+
73+
it("should not retry on non-retriable RangeError", async () => {
5574
let attempt = 0;
5675
const failForAllAttempts = () => {
5776
attempt += 1;
@@ -68,4 +87,22 @@ describe("startup", function () {
6887
})).to.be.rejectedWith("Non-retriable Test Error");
6988
expect(attempt).eq(1);
7089
});
90+
91+
it("should not retry on non-retriable AuthenticationError", async () => {
92+
let attempt = 0;
93+
const failForAllAttempts = () => {
94+
attempt += 1;
95+
throw new AuthenticationError(400, "Test Error");
96+
};
97+
mockAppConfigurationClientListConfigurationSettings(
98+
[[{key: "TestKey", value: "TestValue"}].map(createMockedKeyValue)],
99+
failForAllAttempts);
100+
101+
await expect(load(createMockedConnectionString(), {
102+
startupOptions: {
103+
timeoutInMs: 10_000
104+
}
105+
})).to.be.rejectedWith("authority_not_found");
106+
expect(attempt).eq(1);
107+
});
71108
});

0 commit comments

Comments
 (0)