Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a0a551e
wip
zhiyuanliang-ms Sep 7, 2025
d0ecd09
load from azure front door
zhiyuanliang-ms Sep 7, 2025
8d06357
fix bug
zhiyuanliang-ms Sep 8, 2025
3310171
add more test
zhiyuanliang-ms Sep 8, 2025
6878b64
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 8, 2025
9af1749
add browser test
zhiyuanliang-ms Sep 8, 2025
f97ae3d
update
zhiyuanliang-ms Sep 8, 2025
264113d
update
zhiyuanliang-ms Sep 8, 2025
58d1f2e
fix test
zhiyuanliang-ms Sep 8, 2025
725dd63
remove sync-token header
zhiyuanliang-ms Sep 9, 2025
665af4c
wip
zhiyuanliang-ms Sep 9, 2025
9d63b7c
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 10, 2025
5087206
update
zhiyuanliang-ms Sep 10, 2025
339b3fa
fix lint
zhiyuanliang-ms Sep 10, 2025
b2b76ed
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 10, 2025
1577bef
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 26, 2025
87f952b
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Oct 2, 2025
4480562
update CDN tag
zhiyuanliang-ms Oct 14, 2025
d3c5f96
Merge branch 'zhiyuanliang/afd-support' of https://github.com/Azure/A…
zhiyuanliang-ms Nov 4, 2025
414a8c8
disallow sentinel key refresh for AFD
zhiyuanliang-ms Nov 5, 2025
bebf549
update
zhiyuanliang-ms Nov 5, 2025
564f16a
update
zhiyuanliang-ms Nov 6, 2025
28d37c2
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 7, 2025
ca056b9
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 7, 2025
b5f26c4
update
zhiyuanliang-ms Nov 7, 2025
5b09aee
update
zhiyuanliang-ms Nov 7, 2025
dcf5814
resolve merge conflict
zhiyuanliang-ms Nov 8, 2025
5d9477b
update error message
zhiyuanliang-ms Nov 8, 2025
2599b6a
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 9, 2025
29f7958
update
zhiyuanliang-ms Nov 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"playwright": "^1.55.0"
},
"dependencies": {
"@azure/app-configuration": "^1.9.0",
"@azure/app-configuration": "^1.9.2",
"@azure/core-rest-pipeline": "^1.6.0",
"@azure/identity": "^4.2.1",
"@azure/keyvault-secrets": "^4.7.0",
Expand Down
35 changes: 35 additions & 0 deletions src/afd/afdRequestPipelinePolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { PipelinePolicy } from "@azure/core-rest-pipeline";

/**
* The pipeline policy that remove the authorization header from the request to allow anonymous access to the Azure Front Door.
* @remarks
* The policy position should be perRetry, since it should be executed after the "Sign" phase: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/src/serviceClient.ts
*/
export class AnonymousRequestPipelinePolicy implements PipelinePolicy {
name: string = "AppConfigurationAnonymousRequestPolicy";

async sendRequest(request, next) {
if (request.headers.has("authorization")) {
request.headers.delete("authorization");
}
return next(request);
}
}

/**
* The pipeline policy that remove the "sync-token" header from the request.
* The policy position should be perRetry. It should be executed after the SyncTokenPolicy in @azure/app-configuration, which is executed after retry phase: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts#L198
*/
export class RemoveSyncTokenPipelinePolicy implements PipelinePolicy {
name: string = "AppConfigurationRemoveSyncTokenPolicy";

async sendRequest(request, next) {
if (request.headers.has("sync-token")) {
request.headers.delete("sync-token");
}
return next(request);
}
}
4 changes: 4 additions & 0 deletions src/afd/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export const X_MS_DATE_HEADER = "x-ms-date";
69 changes: 58 additions & 11 deletions src/appConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import {
GetSnapshotOptions,
ListConfigurationSettingsForSnapshotOptions,
GetSnapshotResponse,
KnownSnapshotComposition
KnownSnapshotComposition,
ListConfigurationSettingPage
} from "@azure/app-configuration";
import { isRestError } from "@azure/core-rest-pipeline";
import { isRestError, RestError } from "@azure/core-rest-pipeline";
import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./appConfiguration.js";
import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js";
import { IKeyValueAdapter } from "./keyValueAdapter.js";
Expand Down Expand Up @@ -67,6 +68,7 @@ import { ConfigurationClientManager } from "./configurationClientManager.js";
import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js";
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/errors.js";
import { ErrorMessages } from "./common/errorMessages.js";
import { X_MS_DATE_HEADER } from "./afd/constants.js";

const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds

Expand Down Expand Up @@ -128,12 +130,17 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
// Load balancing
#lastSuccessfulEndpoint: string = "";

// Azure Front Door
#isAfdUsed: boolean = false;

constructor(
clientManager: ConfigurationClientManager,
options: AzureAppConfigurationOptions | undefined,
isAfdUsed: boolean
) {
this.#options = options;
this.#clientManager = clientManager;
this.#isAfdUsed = isAfdUsed;

// enable request tracing if not opt-out
this.#requestTracingEnabled = requestTracingEnabled();
Expand Down Expand Up @@ -221,7 +228,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
isFailoverRequest: this.#isFailoverRequest,
featureFlagTracing: this.#featureFlagTracing,
fmVersion: this.#fmVersion,
aiConfigurationTracing: this.#aiConfigurationTracing
aiConfigurationTracing: this.#aiConfigurationTracing,
isAfdUsed: this.#isAfdUsed
};
}

Expand Down Expand Up @@ -490,7 +498,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
* If false, loads key-value using the key-value selectors. Defaults to false.
*/
async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise<ConfigurationSetting[]> {
const selectors = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;
const selectors: PagedSettingsWatcher[] = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;

// Use a Map to deduplicate configuration settings by key. When multiple selectors return settings with the same key,
// the configuration setting loaded by the later selector in the iteration order will override the one from the earlier selector.
Expand All @@ -509,6 +517,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
tagsFilter: selector.tagFilters
};
const { items, pageWatchers } = await this.#listConfigurationSettings(listOptions);

selector.pageWatchers = pageWatchers;
settings = items;
} else { // snapshot selector
Expand Down Expand Up @@ -675,7 +684,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
return Promise.resolve(false);
}

const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors);
let needRefresh = false;
needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors);

if (needRefresh) {
await this.#loadFeatureFlags();
}
Expand Down Expand Up @@ -718,21 +729,38 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: selector.keyFilter,
labelFilter: selector.labelFilter,
tagsFilter: selector.tagFilters,
pageEtags: pageWatchers.map(w => w.etag ?? "")
tagsFilter: selector.tagFilters
};

if (!this.#isAfdUsed) {
// if AFD is not used, add page etags to the listOptions to send conditional request
listOptions.pageEtags = pageWatchers.map(w => w.etag ?? "") ;
}

const pageIterator = listConfigurationSettingsWithTrace(
this.#requestTraceOptions,
client,
listOptions
).byPage();

let i = 0;
for await (const page of pageIterator) {
// when conditional request is sent, the response will be 304 if not changed
if (page._response.status === 200) { // created or changed
const serverResponseTime: Date = this.#getMsDateHeader(page);
if (i >= pageWatchers.length) {
return true;
}

const lastServerResponseTime = pageWatchers[i].lastServerResponseTime;
let isResponseFresh = false;
if (lastServerResponseTime !== undefined) {
isResponseFresh = serverResponseTime > lastServerResponseTime;
}
if (isResponseFresh &&
page._response.status === 200 && // conditional request returns 304 if not changed
page.etag !== pageWatchers[i].etag) {
return true;
}
i++;
}
}
return false;
Expand All @@ -743,7 +771,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}

/**
* Gets a configuration setting by key and label.If the setting is not found, return undefine instead of throwing an error.
* Gets a configuration setting by key and label. If the setting is not found, return undefined instead of throwing an error.
*/
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
const funcToExecute = async (client) => {
Expand Down Expand Up @@ -779,7 +807,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {

const items: ConfigurationSetting[] = [];
for await (const page of pageIterator) {
pageWatchers.push({ etag: page.etag });
pageWatchers.push({ etag: page.etag, lastServerResponseTime: this.#getMsDateHeader(page) });
items.push(...page.items);
}
return { items, pageWatchers };
Expand Down Expand Up @@ -1107,6 +1135,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
return first15Bytes.toString("base64url");
}
}

/**
* Extracts the response timestamp (x-ms-date) from the response headers. If not found, returns the current time.
*/
#getMsDateHeader(response: GetConfigurationSettingResponse | ListConfigurationSettingPage | RestError): Date {
let header: string | undefined;
if (isRestError(response)) {
header = response.response?.headers?.get(X_MS_DATE_HEADER);
} else {
header = response._response?.headers?.get(X_MS_DATE_HEADER);
}
if (header !== undefined) {
const date = new Date(header);
if (!isNaN(date.getTime())) {
return date;
}
}
return new Date();
}
}

function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {
Expand Down
3 changes: 3 additions & 0 deletions src/common/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const enum ErrorMessages {
INVALID_LABEL_FILTER = "The characters '*' and ',' are not supported in label filters.",
INVALID_TAG_FILTER = "Tag filter must follow the format 'tagName=tagValue'",
CONNECTION_STRING_OR_ENDPOINT_MISSED = "A connection string or an endpoint with credential must be specified to create a client.",
REPLICA_DISCOVERY_NOT_SUPPORTED = "Replica discovery is not supported when loading from Azure Front Door. For guidance on how to take advantage of geo-replication when Azure Front Door is used, visit https://aka.ms/appconfig/geo-replication-with-afd",
LOAD_BALANCING_NOT_SUPPORTED = "Load balancing is not supported when loading from Azure Front Door. For guidance on how to take advantage of geo-replication when Azure Front Door is used, visit https://aka.ms/appconfig/geo-replication-with-afd",
WATCHED_SETTINGS_NOT_SUPPORTED = "Specifying watched settings is not supported when loading from Azure Front Door. If refresh is enabled, all loaded configuration settings will be watched automatically."
}

export const enum KeyVaultReferenceErrorMessages {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

export { AzureAppConfiguration } from "./appConfiguration.js";
export { Disposable } from "./common/disposable.js";
export { load } from "./load.js";
export { load, loadFromAzureFrontDoor } from "./load.js";
export { KeyFilter, LabelFilter } from "./types.js";
export { VERSION } from "./version.js";
48 changes: 46 additions & 2 deletions src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ import { AzureAppConfiguration } from "./appConfiguration.js";
import { AzureAppConfigurationImpl } from "./appConfigurationImpl.js";
import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js";
import { ConfigurationClientManager } from "./configurationClientManager.js";
import { AnonymousRequestPipelinePolicy, RemoveSyncTokenPipelinePolicy } from "./afd/afdRequestPipelinePolicy.js";
import { instanceOfTokenCredential } from "./common/utils.js";
import { ArgumentError } from "./common/errors.js";
import { ErrorMessages } from "./common/errorMessages.js";

const MIN_DELAY_FOR_UNHANDLED_ERROR_IN_MS: number = 5_000;

// Empty token credential to be used when loading from Azure Front Door
const emptyTokenCredential: TokenCredential = {
getToken: async () => ({ token: "", expiresOnTimestamp: Number.MAX_SAFE_INTEGER })
};

/**
* Loads the data from Azure App Configuration service and returns an instance of AzureAppConfiguration.
* @param connectionString The connection string for the App Configuration store.
Expand All @@ -19,7 +27,7 @@ export async function load(connectionString: string, options?: AzureAppConfigura

/**
* Loads the data from Azure App Configuration service and returns an instance of AzureAppConfiguration.
* @param endpoint The URL to the App Configuration store.
* @param endpoint The App Configuration store endpoint.
* @param credential The credential to use to connect to the App Configuration store.
* @param options Optional parameters.
*/
Expand All @@ -42,7 +50,8 @@ export async function load(
}

try {
const appConfiguration = new AzureAppConfigurationImpl(clientManager, options);
const isAfdUsed: boolean = credentialOrOptions === emptyTokenCredential;
const appConfiguration = new AzureAppConfigurationImpl(clientManager, options, isAfdUsed);
await appConfiguration.load();
return appConfiguration;
} catch (error) {
Expand All @@ -56,3 +65,38 @@ export async function load(
throw error;
}
}

/**
* Loads the data from Azure Front Door and returns an instance of AzureAppConfiguration.
* @param endpoint The Azure Front Door endpoint.
* @param appConfigOptions Optional parameters.
*/
export async function loadFromAzureFrontDoor(endpoint: URL | string, options?: AzureAppConfigurationOptions): Promise<AzureAppConfiguration>;

export async function loadFromAzureFrontDoor(
endpoint: string | URL,
appConfigOptions: AzureAppConfigurationOptions = {}
): Promise<AzureAppConfiguration> {
if (appConfigOptions.replicaDiscoveryEnabled) {
throw new ArgumentError(ErrorMessages.REPLICA_DISCOVERY_NOT_SUPPORTED);
}
if (appConfigOptions.loadBalancingEnabled) {
throw new ArgumentError(ErrorMessages.LOAD_BALANCING_NOT_SUPPORTED);
}
if (appConfigOptions.refreshOptions?.watchedSettings && appConfigOptions.refreshOptions.watchedSettings.length > 0) {
throw new ArgumentError(ErrorMessages.WATCHED_SETTINGS_NOT_SUPPORTED);
}

appConfigOptions.replicaDiscoveryEnabled = false; // Disable replica discovery when loading from Azure Front Door

appConfigOptions.clientOptions = {
...appConfigOptions.clientOptions,
additionalPolicies: [
...(appConfigOptions.clientOptions?.additionalPolicies || []),
{ policy: new AnonymousRequestPipelinePolicy(), position: "perRetry" },
{ policy: new RemoveSyncTokenPipelinePolicy(), position: "perRetry" }
]
};

return await load(endpoint, emptyTokenCredential, appConfigOptions);
}
1 change: 1 addition & 0 deletions src/requestTracing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const REPLICA_COUNT_KEY = "ReplicaCount";
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
export const KEY_VAULT_REFRESH_CONFIGURED_TAG = "RefreshesKeyVault";
export const FAILOVER_REQUEST_TAG = "Failover";
export const AFD_USED_TAG = "AFD";

// Compact feature tags
export const FEATURES_KEY = "Features";
Expand Down
Loading