Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
2 changes: 2 additions & 0 deletions sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Added query parameter normalization to support Azure Front Door as a CDN. Query parameter keys are now converted to lowercase and sorted alphabetically.

### Breaking Changes

### Bugs Fixed
Expand Down
2 changes: 1 addition & 1 deletion sdk/appconfiguration/azure-appconfiguration/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/appconfiguration/azure-appconfiguration",
"Tag": "python/appconfiguration/azure-appconfiguration_710e235678"
"Tag": "python/appconfiguration/azure-appconfiguration_91b992c0d1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from azure.core.rest import HttpRequest, HttpResponse
from ._azure_appconfiguration_error import ResourceReadOnlyError
from ._azure_appconfiguration_requests import AppConfigRequestsCredentialsPolicy
from ._query_param_policy import QueryParamPolicy
from ._generated import AzureAppConfigurationClient as AzureAppConfigurationClientGenerated
from ._generated.models import (
SnapshotStatus,
Expand Down Expand Up @@ -65,6 +66,7 @@ def __init__(self, base_url: str, credential: TokenCredential, **kwargs: Any) ->

credential_scopes = [f"{base_url.strip('/')}/.default"]
self._sync_token_policy = SyncTokenPolicy()
self._query_param_policy = QueryParamPolicy()

if isinstance(credential, AzureKeyCredential):
id_credential = kwargs.pop("id_credential")
Expand All @@ -85,7 +87,10 @@ def __init__(self, base_url: str, credential: TokenCredential, **kwargs: Any) ->
)
# mypy doesn't compare the credential type hint with the API surface in patch.py
self._impl = AzureAppConfigurationClientGenerated(
base_url, credential, per_call_policies=self._sync_token_policy, **kwargs # type: ignore[arg-type]
base_url,
credential,
per_call_policies=[self._query_param_policy, self._sync_token_policy],
**kwargs, # type: ignore[arg-type]
)

@classmethod
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# -------------------------------------------------------------------------
from collections import defaultdict
from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode, quote
from azure.core.pipeline.policies import HTTPPolicy


class QueryParamPolicy(HTTPPolicy):
"""Policy to normalize query parameters by converting keys to lowercase and sorting alphabetically.

This policy ensures query parameter keys are converted to lowercase and sorted alphabetically
to support Azure Front Door as a CDN.
"""

def send(self, request):
"""Normalize query parameters before sending the request.

:param request: The pipeline request object
:type request: ~azure.core.pipeline.PipelineRequest
:return: The pipeline response
:rtype: ~azure.core.pipeline.PipelineResponse
"""
try:
# Parse the current URL
parsed_url = urlparse(request.http_request.url)

if parsed_url.query:
# Parse query parameters
query_params = parse_qsl(parsed_url.query, keep_blank_values=True)

# Convert keys to lowercase, drop empty keys
lowered_params = [(key.lower(), value) for key, value in query_params if key]

# Sort all params by key, and for duplicate keys, non-empty values lexicographically, empty values last

grouped = defaultdict(list)
for k, v in lowered_params:
grouped[k].append(v)
normalized_params = []
for k in sorted(grouped.keys()):
values = grouped[k]
if len(values) > 1:
# Empty values last, space values second to last, other non-empty values sorted
# lexicographically first
values = sorted(values, key=self.__sort_key)
normalized_params.extend([(k, v) for v in values])

# Rebuild the query string, encoding spaces as %20 instead of +
new_query = urlencode(normalized_params, quote_via=quote)

# Rebuild the URL with normalized query parameters
new_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
new_query,
parsed_url.fragment,
)
)

# Update the request URL
request.http_request.url = new_url
except (ValueError, TypeError):
# If URL normalization fails due to parsing or encoding errors, continue with the original URL
# This ensures the policy doesn't break existing functionality
pass

return self.next.send(request)

@staticmethod
def __sort_key(v: str):
if v == "":
return (2, "") # empty string last
if v == " ":
return (1, v) # space second to last
return (0, v) # other non-empty values first
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ._sync_token_async import AsyncSyncTokenPolicy
from .._azure_appconfiguration_error import ResourceReadOnlyError
from .._azure_appconfiguration_requests import AppConfigRequestsCredentialsPolicy
from .._query_param_policy import QueryParamPolicy
from .._generated.aio import AzureAppConfigurationClient as AzureAppConfigurationClientGenerated
from .._generated.models import (
SnapshotStatus,
Expand Down Expand Up @@ -69,6 +70,7 @@ def __init__(self, base_url: str, credential: AsyncTokenCredential, **kwargs: An

credential_scopes = [f"{base_url.strip('/')}/.default"]
self._sync_token_policy = AsyncSyncTokenPolicy()
self._query_param_policy = QueryParamPolicy()

if isinstance(credential, AzureKeyCredential):
id_credential = kwargs.pop("id_credential")
Expand All @@ -89,7 +91,10 @@ def __init__(self, base_url: str, credential: AsyncTokenCredential, **kwargs: An
)
# mypy doesn't compare the credential type hint with the API surface in patch.py
self._impl = AzureAppConfigurationClientGenerated(
base_url, credential, per_call_policies=self._sync_token_policy, **kwargs # type: ignore[arg-type]
base_url,
credential,
per_call_policies=[self._query_param_policy, self._sync_token_policy],
**kwargs, # type: ignore[arg-type]
)

@classmethod
Expand Down
Loading
Loading