Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
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 @@ -67,7 +67,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 isUpToDate = true;
if (lastServerResponseTime !== undefined) {
isUpToDate = serverResponseTime > lastServerResponseTime;
}
if (isUpToDate &&
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.",
Copy link
Member

@avanigupta avanigupta Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
REPLICA_DISCOVERY_NOT_SUPPORTED = "Replica discovery is not supported when loading from Azure Front Door.",
REPLICA_DISCOVERY_NOT_SUPPORTED = "Replica discovery is not supported when loading from Azure Front Door. To leverage AppConfig replicas, add multiple replicas to your Front Door origin group. Lear more about [Origins and origin groups in Azure Front Door](https://learn.microsoft.com/en-us/azure/frontdoor/origin?pivots=front-door-standard-premium).",

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error message is too long. And I am not a fan of putting the doc link here.

Instead of pointing to AFD doc, I think the link should be pointed to one doc maintained by ourselves.

Copy link
Member

@avanigupta avanigupta Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a concept owned by AFD, we dont have any doc on this topic. When we choose not to support it ourselves, it’s important to provide users with any available alternatives. I agree its long, but its necessary in this case.
We can reduce the text to:

"Replica discovery isn’t supported via Azure Front Door. To use AppConfig replicas, add multiple replicas to your Front Door origin group. Learn more: https://learn.microsoft.com/en-us/azure/frontdoor/origin"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @jimmyca15 If you agree, we should update the error message in .net provider too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a concept owned by AFD, we dont have any doc on this topic. When we choose not to support it ourselves, it’s important to provide users with any available alternatives. I agree its long, but its necessary in this case. We can reduce the text to:

"Replica discovery isn’t supported via Azure Front Door. To use AppConfig replicas, add multiple replicas to your Front Door origin group. Learn more: https://learn.microsoft.com/en-us/azure/frontdoor/origin"

I mean we can a doc that teach people how to use AFD with app config or a section of FAQ. Our doc can reference this AFD doc. We should have the full control of the doc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will eventually add a doc, but it will not be available when this library is released. So for now, we need to add AFD doc link.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendations can change, exception message lives forever in this version of the sdk. So it's a balance. In this case, I'd say this particular recommendation has to live in a doc. Sounds too much like something subject to change.

In contrast to something like "to use this feature, you must set xyz flag to true"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this version of SDK, we have turned off replica discovery and load balancing. I think there should be some indication of why or what are the alternatives. I'm ok if we dont want to put the doc link at all, but we should at least hint at the origin groups concept. In this case, it's not just a recommendation - it's the only way someone can achieve resiliency using replicas.

Copy link
Member

@jimmyca15 jimmyca15 Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind putting a link, but it should be a link to our docs. Perhaps our geo-replication doc, and we add a blurb about when consuming through AFD, origin groups should be set up.

Something like "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"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. The only downside of using our doc is that provider will be released before we can make any doc updates. But it's not that bad since we will eventually update our docs after ignite.

LOAD_BALANCING_NOT_SUPPORTED = "Load balancing is not supported when loading from Azure Front Door.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LOAD_BALANCING_NOT_SUPPORTED = "Load balancing is not supported when loading from Azure Front Door.",
LOAD_BALANCING_NOT_SUPPORTED = "Load balancing is not supported when loading from Azure Front Door. To load balance between AppConfig replicas, add multiple replicas to your Front Door origin group. Lear more about [Origins and origin groups in Azure Front Door](https://learn.microsoft.com/en-us/azure/frontdoor/origin?pivots=front-door-standard-premium).",

Copy link
Member

@avanigupta avanigupta Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I can make my suggestion shorter:

"Load balancing isn’t supported via Azure Front Door. To load balance across AppConfig replicas, add multiple replicas to your Front Door origin group. Learn more: https://learn.microsoft.com/en-us/azure/frontdoor/origin"

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