Skip to content

Commit b0ab944

Browse files
append etag to url
1 parent 0fb81da commit b0ab944

File tree

4 files changed

+110
-16
lines changed

4 files changed

+110
-16
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,16 @@ import { RefreshTimer } from "./refresh/RefreshTimer.js";
3636
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
3737
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
3838
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
39+
import { ETAG_LOOKUP_HEADER } from "./EtagUrlPipelinePolicy.js";
3940

4041
type PagedSettingSelector = SettingSelector & {
42+
pageEtags?: string[];
43+
4144
/**
42-
* Key: page eTag, Value: feature flag configurations
45+
* The etag which has changed after the last refresh. This is used to break the CDN cache.
46+
* It can either be a page etag or etag of a watched setting.
4347
*/
44-
pageEtags?: string[];
48+
latestEtag?: string;
4549
};
4650

4751
export class AzureAppConfigurationImpl implements AzureAppConfiguration {
@@ -59,7 +63,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
5963
readonly #requestTracingEnabled: boolean;
6064
#clientManager: ConfigurationClientManager;
6165
#options: AzureAppConfigurationOptions | undefined;
62-
#isCdnUsed: boolean;
6366
#isInitialLoadCompleted: boolean = false;
6467
#isFailoverRequest: boolean = false;
6568

@@ -91,6 +94,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
9194
// Load balancing
9295
#lastSuccessfulEndpoint: string = "";
9396

97+
// CDN
98+
#isCdnUsed: boolean;
99+
/**
100+
* The etag of a watched setting which has changed after the last refresh. This is used to break the CDN cache.
101+
* This property will not be used when using key value collection based refresh. It could only be used during updateWatchedKeyValuesEtag and refreshKeyValues.
102+
*/
103+
#latestEtag?: string;
104+
94105
constructor(
95106
clientManager: ConfigurationClientManager,
96107
options: AzureAppConfigurationOptions | undefined,
@@ -218,11 +229,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
218229

219230
/**
220231
* Loads the configuration store for the first time.
232+
* @internal
221233
*/
222234
async load() {
223235
await this.#loadSelectedAndWatchedKeyValues();
236+
if (this.#watchAll) {
237+
this.#kvSelectors.forEach(selector => selector.latestEtag = selector.pageEtags ? selector.pageEtags[0] : undefined);
238+
} else if (this.#refreshEnabled) {
239+
this.#latestEtag = this.#sentinels.find(s => s.etag !== undefined)?.etag;
240+
}
241+
224242
if (this.#featureFlagEnabled) {
225243
await this.#loadFeatureFlags();
244+
if (this.#featureFlagRefreshEnabled) {
245+
this.#ffSelectors.forEach(selector => selector.latestEtag = selector.pageEtags ? selector.pageEtags[0] : undefined);
246+
}
226247
}
227248
// Mark all settings have loaded at startup.
228249
this.#isInitialLoadCompleted = true;
@@ -357,11 +378,26 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
357378
);
358379

359380
for (const selector of selectorsToUpdate) {
360-
const listOptions: ListConfigurationSettingsOptions = {
381+
let listOptions: ListConfigurationSettingsOptions = {
361382
keyFilter: selector.keyFilter,
362-
labelFilter: selector.labelFilter
383+
labelFilter: selector.labelFilter,
363384
};
364385

386+
if (this.#isCdnUsed) {
387+
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
388+
if (this.#watchAll && selector.latestEtag) {
389+
listOptions = {
390+
...listOptions,
391+
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selector.latestEtag }}
392+
};
393+
} else if (this.#latestEtag) {
394+
listOptions = {
395+
...listOptions,
396+
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#latestEtag }}
397+
};
398+
}
399+
}
400+
365401
const pageEtags: string[] = [];
366402
const pageIterator = listConfigurationSettingsWithTrace(
367403
this.#requestTraceOptions,
@@ -422,8 +458,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
422458
sentinel.etag = matchedSetting.etag;
423459
} else {
424460
// Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing
425-
const { key, label } = sentinel;
426-
const response = await this.#getConfigurationSetting({ key, label });
461+
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
462+
const getOptions = this.#isCdnUsed && this.#latestEtag ? { requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#latestEtag }}} : {};
463+
const response = await this.#getConfigurationSetting(sentinel, {...getOptions, onlyIfChanged: false}); // always send non-conditional request
427464
if (response) {
428465
sentinel.etag = response.etag;
429466
} else {
@@ -476,14 +513,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
476513
needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectors);
477514
}
478515
for (const sentinel of this.#sentinels.values()) {
479-
const response = await this.#getConfigurationSetting(sentinel, {
480-
onlyIfChanged: !this.#isCdnUsed // if CDN is used, do not send conditional request
481-
});
516+
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
517+
const getOptions = this.#isCdnUsed && this.#latestEtag ? { requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#latestEtag }}} : {};
518+
const response = await this.#getConfigurationSetting(sentinel, {...getOptions, onlyIfChanged: !this.#isCdnUsed}); // if CDN is used, do not send conditional request
482519

483520
if ((response?.statusCode === 200 && sentinel.etag !== response?.etag) ||
484521
(response === undefined && sentinel.etag !== undefined) // deleted
485522
) {
486523
sentinel.etag = response?.etag;// update etag of the sentinel
524+
this.#latestEtag = response?.etag; // record the last changed etag
487525
needRefresh = true;
488526
break;
489527
}
@@ -527,7 +565,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
527565
const listOptions: ListConfigurationSettingsOptions = {
528566
keyFilter: selector.keyFilter,
529567
labelFilter: selector.labelFilter,
530-
...(!this.#isCdnUsed && { pageEtags: selector.pageEtags }) // if CDN is used, do not send conditional request
568+
...(!this.#isCdnUsed && { pageEtags: selector.pageEtags }), // if CDN is used, do not send conditional request
569+
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
570+
...(this.#isCdnUsed && selector.latestEtag && { requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selector.latestEtag }}})
531571
};
532572

533573
const pageIterator = listConfigurationSettingsWithTrace(
@@ -542,8 +582,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
542582

543583
let i = 0;
544584
for await (const page of pageIterator) {
545-
if (i > selector.pageEtags.length + 1 || // new page
585+
if (i > selector.pageEtags.length + 1 || // new page
546586
(page._response.status === 200 && page.etag !== selector.pageEtags[i])) { // page changed
587+
selector.latestEtag = page.etag; // record the last changed etag
547588
return true;
548589
}
549590
i++;

src/EtagUrlPipelinePolicy.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { PipelinePolicy } from "@azure/core-rest-pipeline";
5+
6+
export const ETAG_LOOKUP_HEADER = "Etag-Lookup";
7+
8+
/**
9+
* The pipeline policy that retrieves the etag from the request header and appends it to the request URL. After that the etag header is removed from the request.
10+
* @remarks
11+
* The policy position should be perCall.
12+
* The App Configuration service will not recognize the etag query parameter in the url, but this can help to break the CDN cache as the cache entry is based on the URL.
13+
*/
14+
export class EtagUrlPipelinePolicy implements PipelinePolicy {
15+
name: string = "AppConfigurationEtagUrlPolicy";
16+
17+
async sendRequest(request, next) {
18+
if (request.headers.has(ETAG_LOOKUP_HEADER)) {
19+
const etag = request.headers.get(ETAG_LOOKUP_HEADER);
20+
request.headers.delete(ETAG_LOOKUP_HEADER);
21+
22+
const url = new URL(request.url);
23+
url.searchParams.append("etag", etag);
24+
request.url = url.toString();
25+
}
26+
27+
return next(request);
28+
}
29+
}

src/load.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AzureAppConfiguration } from "./AzureAppConfiguration.js";
66
import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js";
77
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
88
import { ConfigurationClientManager, instanceOfTokenCredential } from "./ConfigurationClientManager.js";
9+
import { EtagUrlPipelinePolicy } from "./EtagUrlPipelinePolicy.js";
910

1011
const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds
1112

@@ -75,8 +76,17 @@ export async function loadFromCdn(
7576
if (appConfigOptions === undefined) {
7677
appConfigOptions = { clientOptions: {}};
7778
}
78-
// Specify the api version that supports sas token authentication
79-
appConfigOptions.clientOptions = { ...appConfigOptions.clientOptions, apiVersion: "2024-09-01-preview"};
79+
80+
appConfigOptions.clientOptions = {
81+
...appConfigOptions.clientOptions,
82+
// Specify the api version that supports sas token authentication
83+
apiVersion: "2024-09-01-preview",
84+
// Add etag url policy to append etag to the request url for breaking CDN cache
85+
additionalPolicies: [
86+
...(appConfigOptions.clientOptions?.additionalPolicies || []),
87+
{ policy: new EtagUrlPipelinePolicy(), position: "perCall" }
88+
]
89+
};
8090

8191
return await load(cdnEndpoint, emptyTokenCredential, appConfigOptions);
8292
}

src/requestTracing/utils.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ export function listConfigurationSettingsWithTrace(
4343
const actualListOptions = { ...listOptions };
4444
if (requestTracingEnabled) {
4545
actualListOptions.requestOptions = {
46+
...actualListOptions.requestOptions,
4647
customHeaders: {
47-
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted, isCdnUsed, isFailoverRequest)
48+
...(actualListOptions.requestOptions?.customHeaders || {}),
49+
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(
50+
appConfigOptions,
51+
initialLoadCompleted,
52+
isCdnUsed,
53+
isFailoverRequest
54+
)
4855
}
4956
};
5057
}
@@ -69,8 +76,15 @@ export function getConfigurationSettingWithTrace(
6976

7077
if (requestTracingEnabled) {
7178
actualGetOptions.requestOptions = {
79+
...actualGetOptions.requestOptions,
7280
customHeaders: {
73-
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted, isCdnUsed, isFailoverRequest)
81+
...(actualGetOptions.requestOptions?.customHeaders || {}),
82+
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(
83+
appConfigOptions,
84+
initialLoadCompleted,
85+
isCdnUsed,
86+
isFailoverRequest
87+
)
7488
}
7589
};
7690
}

0 commit comments

Comments
 (0)