Skip to content

Load From Azure Front Door #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 72 commits into
base: preview
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
ce9f550
use pipeline policy to ensure cdn request uses correct api version
zhiyuanliang-ms Nov 4, 2024
2adadc7
fix lint & add comments
zhiyuanliang-ms Nov 4, 2024
30108ab
update
zhiyuanliang-ms Nov 5, 2024
197db89
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 5, 2024
04d8a2c
update
zhiyuanliang-ms Nov 7, 2024
bf99b31
fix lint
zhiyuanliang-ms Nov 7, 2024
14364c5
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 10, 2024
62ac2b9
add request tracing for cdn
zhiyuanliang-ms Nov 10, 2024
bd1c875
only send conditional request when cdn is not used
zhiyuanliang-ms Nov 14, 2024
0856707
add testcase
zhiyuanliang-ms Nov 14, 2024
2b78b27
fix lint
zhiyuanliang-ms Nov 15, 2024
fdd30e2
refresh based on page etag
zhiyuanliang-ms Nov 18, 2024
c886936
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 18, 2024
2a7399b
merge preview
zhiyuanliang-ms Nov 18, 2024
b4afe7a
merge preview
zhiyuanliang-ms Nov 18, 2024
50fc5b6
remove watchAll & reorganize the code
zhiyuanliang-ms Nov 19, 2024
70bbdee
add testcase
zhiyuanliang-ms Nov 19, 2024
d8103a5
fix lint & update method name
zhiyuanliang-ms Nov 19, 2024
e123f0e
resolve merge conflict
zhiyuanliang-ms Nov 19, 2024
6958ad5
add comment
zhiyuanliang-ms Nov 19, 2024
635ca48
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Nov 19, 2024
61e4a65
not use conditional request
zhiyuanliang-ms Nov 19, 2024
4f36a1c
fix vulnerability
zhiyuanliang-ms Nov 20, 2024
d325127
resolve merge conflict
zhiyuanliang-ms Dec 2, 2024
647c7a6
resolve conflict
zhiyuanliang-ms Dec 2, 2024
b74e983
update variable name
zhiyuanliang-ms Dec 2, 2024
eec7114
merge
zhiyuanliang-ms Dec 2, 2024
01a7034
move public method
zhiyuanliang-ms Dec 2, 2024
0fb81da
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Dec 2, 2024
b0ab944
append etag to url
zhiyuanliang-ms Dec 3, 2024
c094801
update
zhiyuanliang-ms Dec 4, 2024
bbf1938
update
zhiyuanliang-ms Dec 4, 2024
f3ac831
add more comments
zhiyuanliang-ms Dec 5, 2024
e6fea3c
merge preview
zhiyuanliang-ms Dec 13, 2024
072bcdf
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Dec 13, 2024
356e664
fix lint
zhiyuanliang-ms Dec 13, 2024
2b95ab4
resolve merge conflict
zhiyuanliang-ms Dec 13, 2024
d3c2799
resolve merge conflict
zhiyuanliang-ms Dec 18, 2024
290d338
merge preview
zhiyuanliang-ms Dec 18, 2024
ea7709e
merge
zhiyuanliang-ms Dec 18, 2024
8ed48b5
merge preview
zhiyuanliang-ms Dec 19, 2024
6e34f62
resolve merge conflict
zhiyuanliang-ms Dec 19, 2024
14dbe96
update
zhiyuanliang-ms Dec 19, 2024
8441b32
resolve copilot comment
zhiyuanliang-ms Dec 19, 2024
6fe042a
fix lint
zhiyuanliang-ms Dec 19, 2024
33a5964
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Feb 12, 2025
f23ebe4
Merge branch 'preview' into zhiyuanliang/enforce-api-version-for-cdn
zhiyuanliang-ms Feb 20, 2025
e77d37d
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Apr 29, 2025
41ad554
fix lint
zhiyuanliang-ms Apr 29, 2025
334a047
update testcase
zhiyuanliang-ms Apr 29, 2025
9dec453
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms May 13, 2025
e457605
update
zhiyuanliang-ms Jun 3, 2025
12b0216
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Jun 3, 2025
c096a50
update
zhiyuanliang-ms Jun 4, 2025
811b0e4
fix lint
zhiyuanliang-ms Jun 4, 2025
03d956d
disable replica discovery for CDN
zhiyuanliang-ms Jun 4, 2025
96df9d9
WIP
zhiyuanliang-ms Jun 4, 2025
ac33118
update to latest design
zhiyuanliang-ms Jun 5, 2025
3528e87
update
zhiyuanliang-ms Jun 5, 2025
f688702
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Jun 5, 2025
19b70c2
update
zhiyuanliang-ms Jun 6, 2025
a30b421
update naming
zhiyuanliang-ms Jun 6, 2025
3f05285
update
zhiyuanliang-ms Jun 10, 2025
4f99512
rename api to loadFromAzureFrontDoor
zhiyuanliang-ms Jun 12, 2025
08f4732
update error message
zhiyuanliang-ms Jun 13, 2025
c0ae98a
add policy to remove authorization header
zhiyuanliang-ms Jun 26, 2025
c061b98
add test
zhiyuanliang-ms Jun 26, 2025
9254069
Merge branch 'zhiyuanliang/enforce-api-version-for-cdn' of https://gi…
zhiyuanliang-ms Jun 26, 2025
3005407
fix lint
zhiyuanliang-ms Jun 26, 2025
f367161
update
zhiyuanliang-ms Jun 26, 2025
d74fdd2
add cache break token test
zhiyuanliang-ms Jun 26, 2025
333eb3b
update request tracing testcase
zhiyuanliang-ms Jun 26, 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
33 changes: 28 additions & 5 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 @@ -55,7 +55,7 @@
"uuid": "^9.0.1"
},
"dependencies": {
"@azure/app-configuration": "^1.6.1",
"@azure/app-configuration": "^1.9.0",
"@azure/identity": "^4.2.1",
"@azure/keyvault-secrets": "^4.7.0"
}
Expand Down
205 changes: 126 additions & 79 deletions src/AzureAppConfigurationImpl.ts

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions src/azureFrontDoor/cdnRequestPipelinePolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { PipelinePolicy } from "@azure/core-rest-pipeline";
import { getCryptoModule } from "../common/utils.js";

const CDN_TOKEN_QUERY_PARAMETER = "_";
const RESOURCE_DELETED_PREFIX = "ResourceDeleted";

export const CDN_TOKEN_LOOKUP_HEADER = "cdn-token-lookup";

/**
* The pipeline policy that retrieves the CDN token from the request header and appends it to the request URL. After that the lookup header is removed from the request.
* @remarks
* The policy position should be perCall.
* The App Configuration service will not recognize the CDN token query parameter in the url, but this can help to break the CDN cache as the cache entry is based on the URL.
*/
export class CdnTokenPipelinePolicy implements PipelinePolicy {
name: string = "AppConfigurationCdnTokenPolicy";

async sendRequest(request, next) {
if (request.headers.has(CDN_TOKEN_LOOKUP_HEADER)) {
const token = request.headers.get(CDN_TOKEN_LOOKUP_HEADER);
request.headers.delete(CDN_TOKEN_LOOKUP_HEADER);

const url = new URL(request.url);
url.searchParams.append(CDN_TOKEN_QUERY_PARAMETER, token); // _ is a dummy query parameter to break the CDN cache
request.url = url.toString();
}

return next(request);
}
}

/**
* Calculates a cache consistency token for a deleted resource based on its previous ETag.
* @param etag - The previous ETag of the deleted resource.
*/
export async function calculateResourceDeletedCacheConsistencyToken(etag: string): Promise<string> {
const crypto = getCryptoModule();
const rawString = `${RESOURCE_DELETED_PREFIX}\n${etag}`;
const payload = new TextEncoder().encode(rawString);
// In the browser or Node.js 18+, use crypto.subtle.digest
if (crypto.subtle) {
const hashBuffer = await crypto.subtle.digest("SHA-256", payload);
const hashArray = new Uint8Array(hashBuffer);
const base64String = btoa(String.fromCharCode(...hashArray));
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return base64urlString;
}
// Use the crypto module's hash function
else {
const hash = crypto.createHash("sha256").update(payload).digest();
return hash.toString("base64url");
}
}

/**
* 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);
}
}
23 changes: 23 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export function getCryptoModule(): any {
let crypto;

// Check for browser environment
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
crypto = window.crypto;
}
// Check for Node.js environment
else if (typeof global !== "undefined" && global.crypto) {
crypto = global.crypto;
}
// Fallback to native Node.js crypto module
else {
try {
crypto = require("crypto");
} catch (error) {
console.error("Failed to load the crypto module:", error.message);
throw error;
}
}
return crypto;
}

export function base64Helper(str: string): string {
const bytes = new TextEncoder().encode(str); // UTF-8 encoding
let chars = "";
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 "./AzureAppConfiguration.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";
45 changes: 44 additions & 1 deletion src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ import { AzureAppConfiguration } from "./AzureAppConfiguration.js";
import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js";
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
import { CdnTokenPipelinePolicy, AnonymousRequestPipelinePolicy } from "./azureFrontDoor/cdnRequestPipelinePolicy.js";
import { instanceOfTokenCredential } from "./common/utils.js";
import { ArgumentError } from "./common/error.js";

const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5_000; // 5 seconds

// 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 Down Expand Up @@ -42,7 +49,8 @@ export async function load(
}

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

/**
* Loads the data from Azure Front Door (CDN) and returns an instance of AzureAppConfiguration.
* @param endpoint The URL to the Azure Front Door.
* @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) {
appConfigOptions = {};
}
if (appConfigOptions.replicaDiscoveryEnabled) {
throw new ArgumentError("Replica discovery is not supported when loading from Azure Front Door.");
}
if (appConfigOptions.loadBalancingEnabled) {
throw new ArgumentError("Load balancing is not supported when loading from Azure Front Door.");
}
appConfigOptions.replicaDiscoveryEnabled = false; // Disable replica discovery when loading from Azure Front Door

appConfigOptions.clientOptions = {
...appConfigOptions.clientOptions,
// Add etag url policy to append etag to the request url for breaking CDN cache
additionalPolicies: [
...(appConfigOptions.clientOptions?.additionalPolicies || []),
{ policy: new CdnTokenPipelinePolicy(), position: "perCall" },
{ policy: new AnonymousRequestPipelinePolicy(), 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 @@ -49,6 +49,7 @@ export const REPLICA_COUNT_KEY = "ReplicaCount";

// Tag names
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
export const CDN_USED_TAG = "CDN";
export const FAILOVER_REQUEST_TAG = "Failover";

// Compact feature tags
Expand Down
8 changes: 8 additions & 0 deletions src/requestTracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
HOST_TYPE_KEY,
HostType,
KEY_VAULT_CONFIGURED_TAG,
CDN_USED_TAG,
KUBERNETES_ENV_VAR,
NODEJS_DEV_ENV_VAL,
NODEJS_ENV_VAR,
Expand All @@ -42,6 +43,7 @@ export interface RequestTracingOptions {
initialLoadCompleted: boolean;
replicaCount: number;
isFailoverRequest: boolean;
isCdnUsed: boolean;
featureFlagTracing: FeatureFlagTracingOptions | undefined;
fmVersion: string | undefined;
aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
Expand Down Expand Up @@ -91,7 +93,9 @@ function applyRequestTracing<T extends OperationOptions>(requestTracingOptions:
const actualOptions = { ...operationOptions };
if (requestTracingOptions.enabled) {
actualOptions.requestOptions = {
...actualOptions.requestOptions,
customHeaders: {
...actualOptions.requestOptions?.customHeaders,
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
}
};
Expand All @@ -111,6 +115,7 @@ function createCorrelationContextHeader(requestTracingOptions: RequestTracingOpt
FFFeatures: Seed+Telemetry
UsersKeyVault
Failover
CDN
*/
const keyValues = new Map<string, string | undefined>();
const tags: string[] = [];
Expand Down Expand Up @@ -139,6 +144,9 @@ function createCorrelationContextHeader(requestTracingOptions: RequestTracingOpt
if (requestTracingOptions.isFailoverRequest) {
tags.push(FAILOVER_REQUEST_TAG);
}
if (requestTracingOptions.isCdnUsed) {
tags.push(CDN_USED_TAG);
}
if (requestTracingOptions.replicaCount > 0) {
keyValues.set(REPLICA_COUNT_KEY, requestTracingOptions.replicaCount.toString());
}
Expand Down
3 changes: 2 additions & 1 deletion test/exportedApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export { load } from "../src";
export { load, loadFromAzureFrontDoor } from "../src";
export { CDN_TOKEN_LOOKUP_HEADER } from "../src/azureFrontDoor/cdnRequestPipelinePolicy.js";
15 changes: 10 additions & 5 deletions test/loadBalance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import * as chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
const expect = chai.expect;
import { load } from "./exportedApi.js";
import { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js";
import { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js";
import { AppConfigurationClient } from "@azure/app-configuration";
import { ConfigurationClientWrapper } from "../src/ConfigurationClientWrapper.js";

const mockedKVs = [
{ value: "red", key: "app.settings.fontColor" },
{ value: "40", key: "app.settings.fontSize" },
{ value: "30", key: "app.settings.fontSize", label: "prod" }
].map(createMockedKeyValue);
const fakeEndpoint_1 = createMockedEndpoint("fake_1");
const fakeEndpoint_2 = createMockedEndpoint("fake_2");
const fakeClientWrapper_1 = new ConfigurationClientWrapper(fakeEndpoint_1, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint_1)));
Expand All @@ -29,8 +34,8 @@ describe("load balance", function () {

it("should load balance the request when loadBalancingEnabled", async () => {
mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_2, clientRequestCounter_2);

const connectionString = createMockedConnectionString();
const settings = await load(connectionString, {
Expand Down Expand Up @@ -66,8 +71,8 @@ describe("load balance", function () {
clientRequestCounter_1.count = 0;
clientRequestCounter_2.count = 0;
mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_2, clientRequestCounter_2);

const connectionString = createMockedConnectionString();
// loadBalancingEnabled is default to false
Expand Down
Loading