Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7b0cfd7
add policy
zhiyuanliang-ms Sep 16, 2025
1ffceb0
add testcase
zhiyuanliang-ms Sep 17, 2025
bb14d10
remove debug code
zhiyuanliang-ms Sep 17, 2025
d81b70a
Merge branch 'main' of https://github.com/zhiyuanliang-ms/azure-sdk-f…
zhiyuanliang-ms Sep 18, 2025
042e19f
update recording
zhiyuanliang-ms Sep 18, 2025
a3c0992
fix format
zhiyuanliang-ms Sep 18, 2025
b0f8ce2
update
zhiyuanliang-ms Sep 18, 2025
aca2dd9
Merge branch 'main' of https://github.com/zhiyuanliang-ms/azure-sdk-f…
zhiyuanliang-ms Sep 22, 2025
387d730
add test for tag query
zhiyuanliang-ms Sep 25, 2025
1bd5a4e
update
zhiyuanliang-ms Sep 29, 2025
141e4f7
Merge branch 'main' of https://github.com/zhiyuanliang-ms/azure-sdk-f…
zhiyuanliang-ms Sep 29, 2025
857c849
update
zhiyuanliang-ms Sep 29, 2025
6376b8c
update
zhiyuanliang-ms Oct 10, 2025
a68e541
update
zhiyuanliang-ms Oct 15, 2025
3f06358
update
zhiyuanliang-ms Oct 23, 2025
ee92713
Merge branch 'main' of https://github.com/zhiyuanliang-ms/azure-sdk-f…
zhiyuanliang-ms Oct 23, 2025
b461666
update
zhiyuanliang-ms Oct 23, 2025
dcf63a6
update test
zhiyuanliang-ms Oct 23, 2025
ccbb155
update test
zhiyuanliang-ms Oct 23, 2025
5218baf
handle corner case that query doesn't have =
zhiyuanliang-ms Oct 23, 2025
4b538f5
handle corner case that query is empty
zhiyuanliang-ms Oct 23, 2025
43994c3
update testcase
zhiyuanliang-ms Oct 23, 2025
445920a
update testcase
zhiyuanliang-ms Oct 23, 2025
1fdc43a
update testcase
zhiyuanliang-ms Oct 23, 2025
e35413a
update
zhiyuanliang-ms Oct 24, 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
9 changes: 5 additions & 4 deletions sdk/appconfiguration/app-configuration/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Added internal pipeline policy to normalize (case-insensitive alphabetical) ordering of query parameters for deterministic request URLs.

- Support snapshot reference.
- New type for SnapshotReference - `ConfigurationSetting<SnapshotReferenceValue>`
- Upon using `getConfigurationSetting`(or add/update), use `parseSnapshotReference` methods to access the properties (to translate `ConfigurationSetting` into the type above).
Expand All @@ -18,8 +20,9 @@
## 1.9.0 (2025-04-08)

### Features Added
- Include all the changes from 1.9.0-beta.1 version


- Include all the changes from 1.9.0-beta.1 version

### Other Changes

- Update README with a link to [*`@azure/app-configuration-provider`*](https://www.npmjs.com/package/@azure/app-configuration-provider). [#33152](https://github.com/Azure/azure-sdk-for-js/pull/33152)
Expand Down Expand Up @@ -149,7 +152,6 @@ See [`listConfigurationSettings.ts`](https://github.com/Azure/azure-sdk-for-js/t
### Other Changes

- Updated our `@azure/core-tracing` dependency to the latest version (1.0.0).

- Notable changes include Removal of `@opentelemetry/api` as a transitive dependency and ensuring that the active context is properly propagated.
- Customers who would like to continue using OpenTelemetry driven tracing should visit our [OpenTelemetry Instrumentation](https://www.npmjs.com/package/@azure/opentelemetry-instrumentation-azure-sdk) package for instructions.

Expand Down Expand Up @@ -186,7 +188,6 @@ See [`listConfigurationSettings.ts`](https://github.com/Azure/azure-sdk-for-js/t
### Features Added

- Special configuration settings - feature flag and secret reference are now supported. 🎉

- For types, use `ConfigurationSetting<FeatureFlagValue>` and `ConfigurationSetting<SecretReferenceValue>`.
- Use `parseFeatureFlag` and `parseSecretReference` methods to parse the configuration settings into feature flag and secret reference respectively.

Expand Down
2 changes: 1 addition & 1 deletion sdk/appconfiguration/app-configuration/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "js",
"TagPrefix": "js/appconfiguration/app-configuration",
"Tag": "js/appconfiguration/app-configuration_257c4f0dd5"
"Tag": "js/appconfiguration/app-configuration_e47e6a8bab"
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ import type { PagedAsyncIterableIterator, PagedResult } from "@azure/core-paging
import { getPagedAsyncIterator } from "@azure/core-paging";
import type { PipelinePolicy, RestError } from "@azure/core-rest-pipeline";
import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
import { SyncTokens, syncTokenPolicy } from "./internal/synctokenpolicy.js";
import { SyncTokens, syncTokenPolicy } from "./internal/syncTokenPolicy.js";
import { queryParamPolicy } from "./internal/queryParamPolicy.js";
import type { TokenCredential } from "@azure/core-auth";
import { isTokenCredential } from "@azure/core-auth";
import type {
Expand Down Expand Up @@ -196,6 +197,7 @@ export class AppConfigurationClient {
internalClientPipelineOptions,
);
this.client.pipeline.addPolicy(authPolicy, { phase: "Sign" });
this.client.pipeline.addPolicy(queryParamPolicy());
this.client.pipeline.addPolicy(syncTokenPolicy(this._syncTokens), { afterPhase: "Retry" });
}

Expand Down

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type {
PipelinePolicy,
PipelineRequest,
PipelineResponse,
SendRequest,
} from "@azure/core-rest-pipeline";

/**
* Creates a PipelinePolicy that normalizes query parameters:
* - Lowercase names
* - Sort by lowercase name
* - Preserve the relative order of duplicates
*/
export function queryParamPolicy(): PipelinePolicy {
return {
name: "queryParamPolicy",
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
try {
const originalUrl: string = request.url;
const url = new URL(originalUrl);

if (url.search === "") {
return next(request);
}

const params: ParamEntry[] = [];
for (const entry of url.search.substring(1).split("&")) {
if (entry === "") {
continue;
}
const [name, value] = entry.split("=", 2);
params.push({ lowercaseName: name.toLowerCase(), value: value ?? "" });
}

// Modern JavaScript Array.prototype.sort is stable
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#sort_stability
params.sort((a, b) => {
if (a.lowercaseName < b.lowercaseName) {
return -1;
} else if (a.lowercaseName > b.lowercaseName) {
return 1;
}
return 0;
});

const newSearchParams = params
.map(({ lowercaseName, value }) => `${lowercaseName}=${value}`)
.join("&");

const newUrl = url.origin + url.pathname + "?" + newSearchParams + url.hash;
if (newUrl !== originalUrl) {
request.url = newUrl;
}
} catch {
// If anything goes wrong, fall back to sending the original request.
console.log("Failed to normalize query parameters.");
}

return next(request);
},
};
}

interface ParamEntry {
lowercaseName: string;
value: string;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { SyncTokens, parseSyncToken } from "../../../src/internal/synctokenpolicy.js";
import { SyncTokens, parseSyncToken } from "../../../src/internal/syncTokenPolicy.js";
import {
assertThrowsRestError,
createAppConfigurationClientForTests,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { describe, it, expect } from "vitest";
import { queryParamPolicy } from "../../src/internal/queryParamPolicy.js";
import { createPipelineRequest, createHttpHeaders } from "@azure/core-rest-pipeline";
import type { PipelineRequest, PipelineResponse } from "@azure/core-rest-pipeline";

function mockNext(returnStatus: number = 200) {
return async (request: PipelineRequest): Promise<PipelineResponse> => {
return {
request,
headers: createHttpHeaders({ "url-lookup": request.url }),
status: returnStatus,
} as PipelineResponse;
};
}

describe("urlQueryParamsNormalizationPolicy", () => {
it("normalizes query parameters", async () => {
const policy = queryParamPolicy();
const request = createPipelineRequest({
url: "https://example.azconfig.io/kv?api-version=2023-11-01&After=abcdefg&tags=tag3%3Dvalue3&key=*&label=dev&$Select=key&tags=tag2%3Dvalue2&tags=tag1%3Dvalue1",
});
const response = await policy.sendRequest(request, mockNext());
const finalUrl = response.headers.get("url-lookup")!;
expect(
finalUrl.endsWith(
"?$select=key&after=abcdefg&api-version=2023-11-01&key=*&label=dev&tags=tag3%3Dvalue3&tags=tag2%3Dvalue2&tags=tag1%3Dvalue1",
),
).toBe(true);
});

it("keeps original encoded parameter value", async () => {
const policy = queryParamPolicy();
const request = createPipelineRequest({
url: "https://example.azconfig.io/kv?key=%25%20%2B&label=%00",
});
const response = await policy.sendRequest(request, mockNext());
const finalUrl = response.headers.get("url-lookup")!;
expect(finalUrl.endsWith("?key=%25%20%2B&label=%00")).toBe(true);
});

it("keeps original order of query parameters", async () => {
const policy = queryParamPolicy();
const request = createPipelineRequest({
url: "https://example.azconfig.io/kv?tags=tag2&api-version=2023-11-01&tags=tag1",
});
const response = await policy.sendRequest(request, mockNext());
const finalUrl = response.headers.get("url-lookup")!;
expect(finalUrl.endsWith("?api-version=2023-11-01&tags=tag2&tags=tag1")).toBe(true);
});

it("keeps empty parameter value", async () => {
const policy = queryParamPolicy();
const request = createPipelineRequest({
url: "https://example.azconfig.io/kv?key=&api-version=2023-11-01&key1&=",
});
const response = await policy.sendRequest(request, mockNext());
const finalUrl = response.headers.get("url-lookup")!;
expect(finalUrl.endsWith("?=&api-version=2023-11-01&key=&key1=")).toBe(true);
});

it("removes redundant &", async () => {
const policy = queryParamPolicy();
const request = createPipelineRequest({
url: "https://example.azconfig.io/kv?b=2&&a=1",
});
const response = await policy.sendRequest(request, mockNext());
const finalUrl = response.headers.get("url-lookup")!;
expect(finalUrl.endsWith("?a=1&b=2")).toBe(true);
});

it("skips when no query parameters are present", async () => {
const policy = queryParamPolicy();
const request = createPipelineRequest({
url: "https://example.azconfig.io/kv?",
});
const response = await policy.sendRequest(request, mockNext());
const finalUrl = response.headers.get("url-lookup")!;
expect(finalUrl.endsWith("/kv?")).toBe(true);
});
});
Loading