Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-azure-core for
openSUSE:Factory checked in at 2026-03-17 19:02:48
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-azure-core (Old)
and /work/SRC/openSUSE:Factory/.python-azure-core.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-azure-core"
Tue Mar 17 19:02:48 2026 rev:63 rq:1339361 version:1.38.3
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-azure-core/python-azure-core.changes
2026-02-21 21:01:10.798969851 +0100
+++
/work/SRC/openSUSE:Factory/.python-azure-core.new.8177/python-azure-core.changes
2026-03-17 19:04:10.439867202 +0100
@@ -1,0 +2,8 @@
+Mon Mar 16 13:02:53 UTC 2026 - John Paul Adrian Glaubitz
<[email protected]>
+
+- New upstream release
+ + Version 1.38.3
+ + For detailed information about changes see the
+ CHANGELOG.md file provided with this package
+
+-------------------------------------------------------------------
Old:
----
azure_core-1.38.2.tar.gz
New:
----
azure_core-1.38.3.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-azure-core.spec ++++++
--- /var/tmp/diff_new_pack.FefOUH/_old 2026-03-17 19:04:11.063893063 +0100
+++ /var/tmp/diff_new_pack.FefOUH/_new 2026-03-17 19:04:11.067893228 +0100
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-azure-core
-Version: 1.38.2
+Version: 1.38.3
Release: 0
Summary: Microsoft Azure Core Library for Python
License: MIT
++++++ azure_core-1.38.2.tar.gz -> azure_core-1.38.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/CHANGELOG.md
new/azure_core-1.38.3/CHANGELOG.md
--- old/azure_core-1.38.2/CHANGELOG.md 2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/CHANGELOG.md 2026-03-12 20:09:29.000000000 +0100
@@ -1,5 +1,16 @@
# Release History
+## 1.38.3 (2026-03-12)
+
+### Bugs Fixed
+
+- Fixed `PipelineClient.format_url` to preserve trailing slash in the base URL
when the URL template is query-string-only (e.g., `?key=value`). #45365
+- Fixed `SensitiveHeaderCleanupPolicy` to persist the `insecure_domain_change`
flag across retries after a cross-domain redirect. #45518
+
+### Other Changes
+
+- Added jitter to token refresh timing in `BearerTokenCredentialPolicy` and
`AsyncBearerTokenCredentialPolicy` to prevent simultaneous token refresh
attempts across multiple processes. This helps mitigate the thundering herd
problem during token refresh operations. #43720
+
## 1.38.2 (2026-02-18)
### Bugs Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/PKG-INFO
new/azure_core-1.38.3/PKG-INFO
--- old/azure_core-1.38.2/PKG-INFO 2026-02-18 03:29:53.482592800 +0100
+++ new/azure_core-1.38.3/PKG-INFO 2026-03-12 20:10:17.493318300 +0100
@@ -1,11 +1,10 @@
Metadata-Version: 2.4
Name: azure-core
-Version: 1.38.2
+Version: 1.38.3
Summary: Microsoft Azure Core Library for Python
-Home-page:
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
-Author: Microsoft Corporation
-Author-email: [email protected]
-License: MIT License
+Author-email: Microsoft Corporation <[email protected]>
+License-Expression: MIT
+Project-URL: Repository,
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
Keywords: azure,azure sdk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python
@@ -16,7 +15,6 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
-Classifier: License :: OSI Approved :: MIT License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
@@ -26,19 +24,7 @@
Requires-Dist: aiohttp>=3.0; extra == "aio"
Provides-Extra: tracing
Requires-Dist: opentelemetry-api~=1.26; extra == "tracing"
-Dynamic: author
-Dynamic: author-email
-Dynamic: classifier
-Dynamic: description
-Dynamic: description-content-type
-Dynamic: home-page
-Dynamic: keywords
-Dynamic: license
Dynamic: license-file
-Dynamic: provides-extra
-Dynamic: requires-dist
-Dynamic: requires-python
-Dynamic: summary
# Azure Core shared client library for Python
@@ -320,9 +306,19 @@
<!-- LINKS -->
[package]: https://pypi.org/project/azure-core/
-
# Release History
+## 1.38.3 (2026-03-12)
+
+### Bugs Fixed
+
+- Fixed `PipelineClient.format_url` to preserve trailing slash in the base URL
when the URL template is query-string-only (e.g., `?key=value`). #45365
+- Fixed `SensitiveHeaderCleanupPolicy` to persist the `insecure_domain_change`
flag across retries after a cross-domain redirect. #45518
+
+### Other Changes
+
+- Added jitter to token refresh timing in `BearerTokenCredentialPolicy` and
`AsyncBearerTokenCredentialPolicy` to prevent simultaneous token refresh
attempts across multiple processes. This helps mitigate the thundering herd
problem during token refresh operations. #43720
+
## 1.38.2 (2026-02-18)
### Bugs Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/azure/core/_version.py
new/azure_core-1.38.3/azure/core/_version.py
--- old/azure_core-1.38.2/azure/core/_version.py 2026-02-18
03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/_version.py 2026-03-12
20:09:29.000000000 +0100
@@ -9,4 +9,4 @@
# regenerated.
# --------------------------------------------------------------------------
-VERSION = "1.38.2"
+VERSION = "1.38.3"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.38.2/azure/core/pipeline/policies/_authentication.py
new/azure_core-1.38.3/azure/core/pipeline/policies/_authentication.py
--- old/azure_core-1.38.2/azure/core/pipeline/policies/_authentication.py
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/policies/_authentication.py
2026-03-12 20:09:29.000000000 +0100
@@ -5,6 +5,7 @@
# -------------------------------------------------------------------------
import time
import base64
+import random
from typing import TYPE_CHECKING, Optional, TypeVar, MutableMapping, Any,
Union, cast
from azure.core.credentials import (
@@ -36,6 +37,38 @@
HTTPResponseType = TypeVar("HTTPResponseType", HttpResponse,
LegacyHttpResponse)
HTTPRequestType = TypeVar("HTTPRequestType", HttpRequest, LegacyHttpRequest)
+DEFAULT_REFRESH_WINDOW_SECONDS = 300 # 5 minutes
+MAX_REFRESH_JITTER_SECONDS = 60 # 1 minute
+
+
+def _should_refresh_token(token: Optional[Union["AccessToken",
"AccessTokenInfo"]], refresh_jitter: int) -> bool:
+ """Check if a new token is needed based on expiry and refresh logic.
+
+ :param token: The current token or None if no token exists
+ :type token: Optional[Union[~azure.core.credentials.AccessToken,
~azure.core.credentials.AccessTokenInfo]]
+ :param int refresh_jitter: The jitter to apply to refresh timing
+ :return: True if a new token is needed, False otherwise
+ :rtype: bool
+ """
+ if not token:
+ return True
+
+ now = time.time()
+ if token.expires_on <= now:
+ return True
+
+ refresh_on = getattr(token, "refresh_on", None)
+
+ if refresh_on:
+ # Apply jitter, but ensure that adding it doesn't push the refresh
time past the actual expiration.
+ # This is a safeguard, as refresh_on is typically well before
expires_on.
+ effective_refresh_time = min(refresh_on + refresh_jitter,
token.expires_on)
+ return effective_refresh_time <= now
+
+ time_until_expiry = token.expires_on - now
+ # Reduce refresh window by jitter to delay refresh and distribute load
+ return time_until_expiry < (DEFAULT_REFRESH_WINDOW_SECONDS -
refresh_jitter)
+
# pylint:disable=too-few-public-methods
class _BearerTokenCredentialPolicyBase:
@@ -54,6 +87,7 @@
self._credential = credential
self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None
self._enable_cae: bool = kwargs.get("enable_cae", False)
+ self._refresh_jitter = 0
@staticmethod
def _enforce_https(request: PipelineRequest[HTTPRequestType]) -> None:
@@ -82,9 +116,7 @@
@property
def _need_new_token(self) -> bool:
- now = time.time()
- refresh_on = getattr(self._token, "refresh_on", None)
- return not self._token or (refresh_on and refresh_on <= now) or
self._token.expires_on - now < 300
+ return _should_refresh_token(self._token, self._refresh_jitter)
def _get_token(self, *scopes: str, **kwargs: Any) -> Union["AccessToken",
"AccessTokenInfo"]:
if self._enable_cae:
@@ -108,6 +140,7 @@
:param str scopes: The type of access needed.
"""
self._token = self._get_token(*scopes, **kwargs)
+ self._refresh_jitter = random.randint(0, MAX_REFRESH_JITTER_SECONDS)
class BearerTokenCredentialPolicy(_BearerTokenCredentialPolicyBase,
HTTPPolicy[HTTPRequestType, HTTPResponseType]):
@@ -182,10 +215,6 @@
raise ex from
HttpResponseError(response=response.http_response)
if request_authorized:
- # if we receive a challenge response, we retrieve a new
token
- # which matches the new target. In this case, we don't
want to remove
- # token from the request so clear the
'insecure_domain_change' tag
- request.context.options.pop("insecure_domain_change",
False)
try:
response = self.next.send(request)
self.on_response(request, response)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.38.2/azure/core/pipeline/policies/_authentication_async.py
new/azure_core-1.38.3/azure/core/pipeline/policies/_authentication_async.py
--- old/azure_core-1.38.2/azure/core/pipeline/policies/_authentication_async.py
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/policies/_authentication_async.py
2026-03-12 20:09:29.000000000 +0100
@@ -3,7 +3,7 @@
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# -------------------------------------------------------------------------
-import time
+import random
import base64
from typing import Any, Awaitable, Optional, cast, TypeVar, Union
@@ -18,6 +18,8 @@
from azure.core.pipeline.policies import AsyncHTTPPolicy
from azure.core.pipeline.policies._authentication import (
_BearerTokenCredentialPolicyBase,
+ _should_refresh_token,
+ MAX_REFRESH_JITTER_SECONDS,
)
from azure.core.pipeline.transport import (
AsyncHttpResponse as LegacyAsyncHttpResponse,
@@ -50,6 +52,7 @@
self._lock_instance = None
self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None
self._enable_cae: bool = kwargs.get("enable_cae", False)
+ self._refresh_jitter = 0
@property
def _lock(self):
@@ -127,10 +130,6 @@
raise ex from
HttpResponseError(response=response.http_response)
if request_authorized:
- # if we receive a challenge response, we retrieve a new
token
- # which matches the new target. In this case, we don't
want to remove
- # token from the request so clear the
'insecure_domain_change' tag
- request.context.options.pop("insecure_domain_change",
False)
try:
response = await self.next.send(request)
except Exception:
@@ -192,9 +191,7 @@
return
def _need_new_token(self) -> bool:
- now = time.time()
- refresh_on = getattr(self._token, "refresh_on", None)
- return not self._token or (refresh_on and refresh_on <= now) or
self._token.expires_on - now < 300
+ return _should_refresh_token(self._token, self._refresh_jitter)
async def _get_token(self, *scopes: str, **kwargs: Any) ->
Union["AccessToken", "AccessTokenInfo"]:
if self._enable_cae:
@@ -226,3 +223,4 @@
:param str scopes: The type of access needed.
"""
self._token = await self._get_token(*scopes, **kwargs)
+ self._refresh_jitter = random.randint(0, MAX_REFRESH_JITTER_SECONDS)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.38.2/azure/core/pipeline/policies/_redirect.py
new/azure_core-1.38.3/azure/core/pipeline/policies/_redirect.py
--- old/azure_core-1.38.2/azure/core/pipeline/policies/_redirect.py
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/policies/_redirect.py
2026-03-12 20:09:29.000000000 +0100
@@ -210,9 +210,8 @@
if domain_changed(original_domain, request.http_request.url):
# "insecure_domain_change" is used to indicate that a
redirect
# has occurred to a different domain. This tells the
SensitiveHeaderCleanupPolicy
- # to clean up sensitive headers. We need to remove it
before sending the request
- # to the transport layer.
- request.context.options["insecure_domain_change"] = True
+ # to clean up sensitive headers.
+ request.context["insecure_domain_change"] = True
continue
return response
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.38.2/azure/core/pipeline/policies/_redirect_async.py
new/azure_core-1.38.3/azure/core/pipeline/policies/_redirect_async.py
--- old/azure_core-1.38.2/azure/core/pipeline/policies/_redirect_async.py
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/policies/_redirect_async.py
2026-03-12 20:09:29.000000000 +0100
@@ -81,9 +81,8 @@
if domain_changed(original_domain, request.http_request.url):
# "insecure_domain_change" is used to indicate that a
redirect
# has occurred to a different domain. This tells the
SensitiveHeaderCleanupPolicy
- # to clean up sensitive headers. We need to remove it
before sending the request
- # to the transport layer.
- request.context.options["insecure_domain_change"] = True
+ # to clean up sensitive headers.
+ request.context["insecure_domain_change"] = True
continue
return response
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.38.2/azure/core/pipeline/policies/_sensitive_header_cleanup_policy.py
new/azure_core-1.38.3/azure/core/pipeline/policies/_sensitive_header_cleanup_policy.py
---
old/azure_core-1.38.2/azure/core/pipeline/policies/_sensitive_header_cleanup_policy.py
2026-02-18 03:29:16.000000000 +0100
+++
new/azure_core-1.38.3/azure/core/pipeline/policies/_sensitive_header_cleanup_policy.py
2026-03-12 20:09:29.000000000 +0100
@@ -72,9 +72,8 @@
"""
# "insecure_domain_change" is used to indicate that a redirect
# has occurred to a different domain. This tells the
SensitiveHeaderCleanupPolicy
- # to clean up sensitive headers. We need to remove it before sending
the request
- # to the transport layer.
- insecure_domain_change =
request.context.options.pop("insecure_domain_change", False)
+ # to clean up sensitive headers.
+ insecure_domain_change = request.context.get("insecure_domain_change",
False)
if not self._disable_redirect_cleanup and insecure_domain_change:
for header in self._blocked_redirect_headers:
request.http_request.headers.pop(header, None)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.38.2/azure/core/pipeline/transport/_base.py
new/azure_core-1.38.3/azure/core/pipeline/transport/_base.py
--- old/azure_core-1.38.2/azure/core/pipeline/transport/_base.py
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/transport/_base.py
2026-03-12 20:09:29.000000000 +0100
@@ -663,7 +663,7 @@
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
try:
- base = self._base_url.format(**kwargs).rstrip("/")
+ base = self._base_url.format(**kwargs)
except KeyError as key:
err_msg = "The value provided for the url part {} was
incorrect, and resulted in an invalid url"
raise ValueError(err_msg.format(key.args[0])) from key
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/azure_core.egg-info/PKG-INFO
new/azure_core-1.38.3/azure_core.egg-info/PKG-INFO
--- old/azure_core-1.38.2/azure_core.egg-info/PKG-INFO 2026-02-18
03:29:53.000000000 +0100
+++ new/azure_core-1.38.3/azure_core.egg-info/PKG-INFO 2026-03-12
20:10:17.000000000 +0100
@@ -1,11 +1,10 @@
Metadata-Version: 2.4
Name: azure-core
-Version: 1.38.2
+Version: 1.38.3
Summary: Microsoft Azure Core Library for Python
-Home-page:
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
-Author: Microsoft Corporation
-Author-email: [email protected]
-License: MIT License
+Author-email: Microsoft Corporation <[email protected]>
+License-Expression: MIT
+Project-URL: Repository,
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
Keywords: azure,azure sdk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python
@@ -16,7 +15,6 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
-Classifier: License :: OSI Approved :: MIT License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
@@ -26,19 +24,7 @@
Requires-Dist: aiohttp>=3.0; extra == "aio"
Provides-Extra: tracing
Requires-Dist: opentelemetry-api~=1.26; extra == "tracing"
-Dynamic: author
-Dynamic: author-email
-Dynamic: classifier
-Dynamic: description
-Dynamic: description-content-type
-Dynamic: home-page
-Dynamic: keywords
-Dynamic: license
Dynamic: license-file
-Dynamic: provides-extra
-Dynamic: requires-dist
-Dynamic: requires-python
-Dynamic: summary
# Azure Core shared client library for Python
@@ -320,9 +306,19 @@
<!-- LINKS -->
[package]: https://pypi.org/project/azure-core/
-
# Release History
+## 1.38.3 (2026-03-12)
+
+### Bugs Fixed
+
+- Fixed `PipelineClient.format_url` to preserve trailing slash in the base URL
when the URL template is query-string-only (e.g., `?key=value`). #45365
+- Fixed `SensitiveHeaderCleanupPolicy` to persist the `insecure_domain_change`
flag across retries after a cross-domain redirect. #45518
+
+### Other Changes
+
+- Added jitter to token refresh timing in `BearerTokenCredentialPolicy` and
`AsyncBearerTokenCredentialPolicy` to prevent simultaneous token refresh
attempts across multiple processes. This helps mitigate the thundering herd
problem during token refresh operations. #43720
+
## 1.38.2 (2026-02-18)
### Bugs Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/azure_core.egg-info/SOURCES.txt
new/azure_core-1.38.3/azure_core.egg-info/SOURCES.txt
--- old/azure_core-1.38.2/azure_core.egg-info/SOURCES.txt 2026-02-18
03:29:53.000000000 +0100
+++ new/azure_core-1.38.3/azure_core.egg-info/SOURCES.txt 2026-03-12
20:10:17.000000000 +0100
@@ -5,7 +5,6 @@
README.md
TROUBLESHOOTING.md
pyproject.toml
-setup.py
azure/__init__.py
azure/core/__init__.py
azure/core/_azure_clouds.py
@@ -85,7 +84,6 @@
azure_core.egg-info/PKG-INFO
azure_core.egg-info/SOURCES.txt
azure_core.egg-info/dependency_links.txt
-azure_core.egg-info/not-zip-safe
azure_core.egg-info/requires.txt
azure_core.egg-info/top_level.txt
doc/azure.core.pipeline.policies.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/azure_core.egg-info/not-zip-safe
new/azure_core-1.38.3/azure_core.egg-info/not-zip-safe
--- old/azure_core-1.38.2/azure_core.egg-info/not-zip-safe 2026-02-18
03:29:53.000000000 +0100
+++ new/azure_core-1.38.3/azure_core.egg-info/not-zip-safe 1970-01-01
01:00:00.000000000 +0100
@@ -1 +0,0 @@
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/pyproject.toml
new/azure_core-1.38.3/pyproject.toml
--- old/azure_core-1.38.2/pyproject.toml 2026-02-18 03:29:16.000000000
+0100
+++ new/azure_core-1.38.3/pyproject.toml 2026-03-12 20:09:29.000000000
+0100
@@ -1,3 +1,50 @@
+[build-system]
+requires = ["setuptools>=77.0.3", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "azure-core"
+authors = [
+ {name = "Microsoft Corporation", email = "[email protected]"},
+]
+description = "Microsoft Azure Core Library for Python"
+keywords = ["azure", "azure sdk"]
+requires-python = ">=3.9"
+license = "MIT"
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+dependencies = [
+ "requests>=2.21.0",
+ "typing-extensions>=4.6.0",
+]
+dynamic = ["version", "readme"]
+
+[project.optional-dependencies]
+aio = ["aiohttp>=3.0"]
+tracing = ["opentelemetry-api~=1.26"]
+
+[project.urls]
+Repository =
"https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core"
+
+[tool.setuptools.dynamic]
+version = {attr = "azure.core._version.VERSION"}
+readme = {file = ["README.md", "CHANGELOG.md"], content-type = "text/markdown"}
+
+[tool.setuptools.packages.find]
+include = ["azure.core*"]
+
+[tool.setuptools.package-data]
+pytyped = ["py.typed"]
+
[tool.azure-sdk-build]
mypy = true
type_check_samples = true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/setup.py
new/azure_core-1.38.3/setup.py
--- old/azure_core-1.38.2/setup.py 2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/setup.py 1970-01-01 01:00:00.000000000 +0100
@@ -1,83 +0,0 @@
-#!/usr/bin/env python
-
-# -------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-# --------------------------------------------------------------------------
-
-import re
-import os.path
-from io import open
-from setuptools import find_packages, setup # type: ignore
-
-# Change the PACKAGE_NAME only to change folder and different name
-PACKAGE_NAME = "azure-core"
-PACKAGE_PPRINT_NAME = "Core"
-
-# a-b-c => a/b/c
-package_folder_path = PACKAGE_NAME.replace("-", "/")
-# a-b-c => a.b.c
-namespace_name = PACKAGE_NAME.replace("-", ".")
-
-# Version extraction inspired from 'requests'
-with open(os.path.join(package_folder_path, "_version.py"), "r") as fd:
- version = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(),
re.MULTILINE).group(1) # type: ignore
-
-if not version:
- raise RuntimeError("Cannot find version information")
-
-with open("README.md", encoding="utf-8") as f:
- readme = f.read()
-with open("CHANGELOG.md", encoding="utf-8") as f:
- changelog = f.read()
-
-setup(
- name=PACKAGE_NAME,
- version=version,
- include_package_data=True,
- description="Microsoft Azure {} Library for
Python".format(PACKAGE_PPRINT_NAME),
- long_description=readme + "\n\n" + changelog,
- long_description_content_type="text/markdown",
- license="MIT License",
- author="Microsoft Corporation",
- author_email="[email protected]",
-
url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core",
- keywords="azure, azure sdk",
- classifiers=[
- "Development Status :: 5 - Production/Stable",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
- "License :: OSI Approved :: MIT License",
- ],
- zip_safe=False,
- packages=find_packages(
- exclude=[
- "tests",
- # Exclude packages that will be covered by PEP420 or nspkg
- "azure",
- ]
- ),
- package_data={
- "pytyped": ["py.typed"],
- },
- python_requires=">=3.9",
- install_requires=[
- "requests>=2.21.0",
- "typing-extensions>=4.6.0",
- ],
- extras_require={
- "aio": [
- "aiohttp>=3.0",
- ],
- "tracing": [
- "opentelemetry-api~=1.26",
- ],
- },
-)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.38.2/tests/async_tests/test_authentication_async.py
new/azure_core-1.38.3/tests/async_tests/test_authentication_async.py
--- old/azure_core-1.38.2/tests/async_tests/test_authentication_async.py
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/tests/async_tests/test_authentication_async.py
2026-03-12 20:09:29.000000000 +0100
@@ -18,8 +18,10 @@
AsyncBearerTokenCredentialPolicy,
SansIOHTTPPolicy,
AsyncRedirectPolicy,
+ AsyncRetryPolicy,
SensitiveHeaderCleanupPolicy,
)
+from azure.core.pipeline.policies._authentication import
MAX_REFRESH_JITTER_SECONDS
from azure.core.pipeline.transport import AsyncHttpTransport, HttpRequest
import pytest
import trio
@@ -244,7 +246,9 @@
await pipeline.run(http_request("GET", "https://spam.eggs"))
assert credential.get_token_info.call_count == 2 # token is expired ->
policy should call get_token_info again
- refreshable_token = AccessTokenInfo("token", int(time.time() + 3600),
refresh_on=int(time.time() - 1))
+ refreshable_token = AccessTokenInfo(
+ "token", int(time.time() + 3600), refresh_on=int(time.time() -
(MAX_REFRESH_JITTER_SECONDS + 5))
+ )
credential.get_token_info.reset_mock()
credential.get_token_info.return_value = refreshable_token
pipeline = AsyncPipeline(transport=AsyncMock(),
policies=[AsyncBearerTokenCredentialPolicy(credential, "scope")])
@@ -735,3 +739,171 @@
# Verify the exception chaining
assert exc_info.value.__cause__ is not None
assert isinstance(exc_info.value.__cause__, HttpResponseError)
+
+
[email protected]
+async def test_jitter_set_on_token_request_async():
+ """Test that _refresh_jitter is set when _request_token is called on the
async policy."""
+ token = AccessToken("test_token", int(time.time()) + 3600)
+
+ credential = AsyncMock(spec_set=["get_token"])
+ credential.get_token.return_value = token
+ policy = AsyncBearerTokenCredentialPolicy(credential, "scope")
+
+ # Initially jitter should be 0
+ assert policy._refresh_jitter == 0
+
+ with
patch("azure.core.pipeline.policies._authentication_async.random.randint") as
mock_randint:
+ mock_randint.return_value = 42
+
+ await policy._request_token("scope")
+
+ assert policy._refresh_jitter == 42
+ mock_randint.assert_called_once_with(0, MAX_REFRESH_JITTER_SECONDS)
+
+ # Test that jitter is updated on subsequent token requests
+ with
patch("azure.core.pipeline.policies._authentication_async.random.randint") as
mock_randint:
+ mock_randint.return_value = 25
+
+ await policy._request_token("scope")
+
+ assert policy._refresh_jitter == 25
+ mock_randint.assert_called_once_with(0, MAX_REFRESH_JITTER_SECONDS)
+
+
[email protected]
+async def test_challenge_auth_header_stripped_after_redirect():
+ """Assuming the SensitiveHeaderCleanupPolicy is in the pipeline, the
authorization header should be stripped after
+ a redirect to a different domain by default, and preserved if the policy
is configured to disable cleanup."""
+
+ class MockTransport(AsyncHttpTransport):
+ def __init__(self, cleanup_disabled=False):
+ self._first = True
+ self._cleanup_disabled = cleanup_disabled
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+ async def close(self):
+ pass
+
+ async def open(self):
+ pass
+
+ async def send(self, request, **kwargs):
+ if self._first:
+ self._first = False
+ assert request.headers["Authorization"] == "Bearer
{}".format(auth_header)
+ response = Response()
+ response.status_code = 307
+ response.headers["location"] =
"https://redirect-target.example.invalid"
+ return response
+
+ # Second request: after redirect
+ if self._cleanup_disabled:
+ assert request.headers.get("Authorization")
+ else:
+ assert not request.headers.get("Authorization")
+ response = Response()
+ response.status_code = 401
+ response.headers["WWW-Authenticate"] = (
+ 'Bearer error="insufficient_claims",
claims="eyJhY2Nlc3NfdG9rZW4iOnsiZm9vIjoiYmFyIn19"'
+ )
+ return response
+
+ auth_header = "token"
+ get_token_call_count = 0
+
+ async def mock_get_token(*_, **__):
+ nonlocal get_token_call_count
+ get_token_call_count += 1
+ return AccessToken(auth_header, 0)
+
+ credential = Mock(spec_set=["get_token"], get_token=mock_get_token)
+ auth_policy = AsyncBearerTokenCredentialPolicy(credential, "scope")
+ redirect_policy = AsyncRedirectPolicy()
+ header_clean_up_policy = SensitiveHeaderCleanupPolicy()
+ pipeline = AsyncPipeline(transport=MockTransport(),
policies=[redirect_policy, auth_policy, header_clean_up_policy])
+
+ response = await pipeline.run(HttpRequest("GET",
"https://legitimate.azure.com"))
+ assert response.http_response.status_code == 401
+
+ header_clean_up_policy =
SensitiveHeaderCleanupPolicy(disable_redirect_cleanup=True)
+ pipeline = AsyncPipeline(
+ transport=MockTransport(cleanup_disabled=True),
+ policies=[redirect_policy, auth_policy, header_clean_up_policy],
+ )
+ response = await pipeline.run(HttpRequest("GET",
"https://legitimate.azure.com"))
+ assert response.http_response.status_code == 401
+
+
[email protected]
+async def test_auth_header_stripped_after_cross_domain_redirect_with_retry():
+ """After a cross-domain redirect, if the redirected-to endpoint returns a
retryable status code,
+ the Authorization header should still be stripped on the retry attempt.
This verifies that the
+ insecure_domain_change flag persists across retries so
SensitiveHeaderCleanupPolicy continues to
+ remove the Authorization header."""
+
+ class MockTransport(AsyncHttpTransport):
+ def __init__(self):
+ self._request_count = 0
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+ async def close(self):
+ pass
+
+ async def open(self):
+ pass
+
+ async def send(self, request, **kwargs):
+ self._request_count += 1
+
+ if self._request_count == 1:
+ # First request: to the original domain — should have auth
header
+ assert request.headers.get("Authorization") == "Bearer
{}".format(auth_header)
+ response = Response()
+ response.status_code = 307
+ response.headers["location"] =
"https://redirect-target.example.invalid"
+ return response
+
+ if self._request_count == 2:
+ # Second request: after redirect to attacker domain — auth
header should be stripped
+ assert not request.headers.get(
+ "Authorization"
+ ), "Authorization header should be stripped on first request
to redirected domain"
+ response = Response()
+ response.status_code = 500
+ return response
+
+ if self._request_count == 3:
+ # Third request: retry to attacker domain — auth header should
STILL be stripped
+ assert not request.headers.get(
+ "Authorization"
+ ), "Authorization header should be stripped on retry to
redirected domain"
+ response = Response()
+ response.status_code = 200
+ return response
+
+ raise RuntimeError("Unexpected request count:
{}".format(self._request_count))
+
+ auth_header = "token"
+
+ async def mock_get_token(*_, **__):
+ return AccessToken(auth_header, 0)
+
+ credential = Mock(spec_set=["get_token"], get_token=mock_get_token)
+ auth_policy = AsyncBearerTokenCredentialPolicy(credential, "scope")
+ redirect_policy = AsyncRedirectPolicy()
+ retry_policy = AsyncRetryPolicy(retry_total=1, retry_backoff_factor=0)
+ header_clean_up_policy = SensitiveHeaderCleanupPolicy()
+ transport = MockTransport()
+ # Pipeline order matches the real default: redirect -> retry -> auth ->
... -> sensitive header cleanup
+ pipeline = AsyncPipeline(
+ transport=transport,
+ policies=[redirect_policy, retry_policy, auth_policy,
header_clean_up_policy],
+ )
+ response = await pipeline.run(HttpRequest("GET",
"https://legitimate.azure.com"))
+ assert response.http_response.status_code == 200
+ assert transport._request_count == 3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/tests/test_authentication.py
new/azure_core-1.38.3/tests/test_authentication.py
--- old/azure_core-1.38.2/tests/test_authentication.py 2026-02-18
03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/tests/test_authentication.py 2026-03-12
20:09:29.000000000 +0100
@@ -22,16 +22,22 @@
from azure.core.pipeline.policies import (
BearerTokenCredentialPolicy,
RedirectPolicy,
+ RetryPolicy,
SansIOHTTPPolicy,
AzureKeyCredentialPolicy,
AzureSasCredentialPolicy,
SensitiveHeaderCleanupPolicy,
)
+from azure.core.pipeline.policies._authentication import (
+ DEFAULT_REFRESH_WINDOW_SECONDS,
+ MAX_REFRESH_JITTER_SECONDS,
+ _should_refresh_token,
+)
from utils import HTTP_REQUESTS
import pytest
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
@@ -193,7 +199,9 @@
pipeline.run(http_request("GET", "https://spam.eggs"))
assert credential.get_token_info.call_count == 2 # token is expired ->
policy should call get_token_info again
- refreshable_token = AccessTokenInfo("token", int(time.time() + 3600),
refresh_on=int(time.time() - 1))
+ refreshable_token = AccessTokenInfo(
+ "token", int(time.time() + 3600), refresh_on=int(time.time() -
(MAX_REFRESH_JITTER_SECONDS + 5))
+ )
credential.get_token_info.reset_mock()
credential.get_token_info.return_value = refreshable_token
pipeline = Pipeline(transport=Mock(),
policies=[BearerTokenCredentialPolicy(credential, "scope")])
@@ -417,33 +425,187 @@
def test_need_new_token():
- expected_scope = "scope"
now = int(time.time())
- policy = BearerTokenCredentialPolicy(Mock(), expected_scope)
-
# Token is expired.
- policy._token = AccessToken("", now - 1200)
- assert policy._need_new_token
+ token = AccessToken("", now - 1200)
+ assert _should_refresh_token(token, 0)
# Token is about to expire within 300 seconds.
- policy._token = AccessToken("", now + 299)
- assert policy._need_new_token
+ token = AccessToken("", now + 299)
+ assert _should_refresh_token(token, 0)
# Token still has more than 300 seconds to live.
- policy._token = AccessToken("", now + 305)
- assert not policy._need_new_token
+ token = AccessToken("", now + 305)
+ assert not _should_refresh_token(token, 0)
# Token has both expires_on and refresh_on set well into the future.
- policy._token = AccessTokenInfo("", now + 1200, refresh_on=now + 1200)
- assert not policy._need_new_token
+ token = AccessTokenInfo("", now + 1200, refresh_on=now + 1200)
+ assert not _should_refresh_token(token, 0)
# Token is not close to expiring, but refresh_on is in the past.
- policy._token = AccessTokenInfo("", now + 1200, refresh_on=now - 1)
- assert policy._need_new_token
+ token = AccessTokenInfo("", now + 1200, refresh_on=now - 1)
+ assert _should_refresh_token(token, 0)
- policy._token = None
- assert policy._need_new_token
+ # No token
+ assert _should_refresh_token(None, 0)
+
+
+def test_need_new_token_with_jitter():
+ """Test that jitter affects token refresh timing for both expires_on and
refresh_on scenarios."""
+ # Mock time.time() to have precise control over timing
+ with patch("azure.core.pipeline.policies._authentication.time") as
mock_time:
+ mock_time.time.return_value = 1000.0 # Fixed current time
+
+ # Test jitter with expires_on based tokens (no refresh_on)
+ # Test with a token that expires in 290 seconds (would normally
trigger refresh)
+ token = AccessToken("", 1290) # 1000 + 290
+
+ # Set jitter to 0 - should need new token (290 <
DEFAULT_REFRESH_WINDOW_SECONDS)
+ assert _should_refresh_token(token, 0)
+
+ # Set jitter to 10 - should NOT need new token (290 < 290 is False)
+ assert not _should_refresh_token(token, 10)
+
+ # Test with a token that expires in 250 seconds
+ token = AccessToken("", 1250) # 1000 + 250
+
+ # With jitter of 0 - should need new token (250 <
DEFAULT_REFRESH_WINDOW_SECONDS)
+ assert _should_refresh_token(token, 0)
+
+ # With jitter of 10 - should need new token (250 < 290)
+ assert _should_refresh_token(token, 10)
+
+ # With max jitter (60) - should NOT need new token (250 < 240 is False)
+ assert not _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+ # Test with a token that expires in 200 seconds
+ token = AccessToken("", 1200) # 1000 + 200
+
+ # Even with max jitter, should need new token (200 < 240)
+ assert _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+
+def test_need_new_token_with_refresh_on_and_jitter():
+ """Test that jitter affects refresh_on based token refresh timing."""
+ with patch("azure.core.pipeline.policies._authentication.time") as
mock_time:
+ mock_time.time.return_value = 1000.0 # Fixed current time
+
+ # Test jitter with refresh_on based tokens
+ # Token expires in 1 hour but should refresh in 5 minutes
+ refresh_time = 1300 # 1000 + 300
+ expiry_time = 4600 # 1000 + 3600
+ token = AccessTokenInfo("", expiry_time, refresh_on=refresh_time)
+
+ # Set jitter to 0 - effective refresh time is 1300, now=1000, so no
refresh needed
+ assert not _should_refresh_token(token, 0)
+
+ # Set jitter to 30 - effective refresh time is 1330, now=1000, so no
refresh needed
+ assert not _should_refresh_token(token, 30)
+
+ # Test with refresh_on in the past
+ token = AccessTokenInfo("", expiry_time, refresh_on=990) # refresh_on
= 1000 - 10
+
+ # With jitter of 5, effective refresh time is min(990 + 5, 4600) =
995, still < 1000
+ assert _should_refresh_token(token, 5)
+
+ # With jitter of 15, effective refresh time is min(990 + 15, 4600) =
1005, now < 1005 so no refresh
+ assert not _should_refresh_token(token, 15)
+
+ # Test jitter capping at expires_on
+ # Token expires soon but refresh_on is very close to expires_on
+ close_expiry = 1030 # 1000 + 30
+ token = AccessTokenInfo("", close_expiry, refresh_on=close_expiry - 5)
+
+ # Large jitter that would normally push refresh time past expiry
+ # Effective refresh time should be capped at expires_on
+ # min(1025 + 60, 1030) = 1030, now=1000 < 1030, so no refresh
+ assert not _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+ # But if we're right at the expiry time, we should need a new token
+ token = AccessTokenInfo("", 1000, refresh_on=995) # expires_on == now
+ # Effective refresh time: min(995 + 60, 1000) = 1000, now=1000 >= 1000
+ assert _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+
+def test_need_new_token_jitter_boundary_conditions():
+ """Test boundary conditions for jitter in token refresh logic."""
+ # Mock time.time() to have precise control over timing
+ with patch("azure.core.pipeline.policies._authentication.time") as
mock_time:
+ mock_time.time.return_value = 1000.0 # Fixed current time
+
+ # Test boundary at DEFAULT_REFRESH_WINDOW_SECONDS (the default refresh
window)
+ token = AccessToken("", 1000 + DEFAULT_REFRESH_WINDOW_SECONDS)
+
+ # Zero jitter - exactly at refresh window boundary, not less than, so
no refresh
+ assert not _should_refresh_token(token, 0)
+
+ # Test at 1 second under the refresh window
+ token = AccessToken("", 1000 + DEFAULT_REFRESH_WINDOW_SECONDS - 1)
+
+ # Zero jitter - 299 seconds remaining, 299 <
DEFAULT_REFRESH_WINDOW_SECONDS = True (refresh)
+ assert _should_refresh_token(token, 0)
+
+ # Small jitter - 299 seconds remaining, 299 < 295 = False (no refresh)
+ assert not _should_refresh_token(token, 5)
+
+ # Test at 250 seconds
+ token = AccessToken("", 1250) # 1000 + 250
+
+ # Zero jitter - 250 seconds remaining, 250 <
DEFAULT_REFRESH_WINDOW_SECONDS = True (refresh)
+ assert _should_refresh_token(token, 0)
+
+ # Small jitter - 250 seconds remaining, 250 < 290 = True (refresh)
+ assert _should_refresh_token(token, 10)
+
+ # Max jitter - 250 seconds remaining, 250 < 240 = False (no refresh)
+ assert not _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+ # Test refresh_on scenarios with jitter
+ # Test refresh_on exactly at current time
+ token = AccessTokenInfo("", 4600, refresh_on=1000) # expires at 1000
+ 3600, refresh at 1000
+ # Effective refresh time is 1000 + 0 = 1000, so 1000 >= 1000 is True
+ assert _should_refresh_token(token, 0)
+
+ # Test refresh_on with positive jitter (delays refresh)
+ token = AccessTokenInfo("", 4600, refresh_on=995) # expires at 1000 +
3600, refresh at 995
+ # Effective refresh time is min(995 + 10, 4600) = 1005, so 1000 < 1005
(no refresh)
+ assert not _should_refresh_token(token, 10)
+
+
+def test_jitter_set_on_token_request():
+ """Test that _refresh_jitter is set when _request_token is called."""
+ expected_scope = "scope"
+ token = AccessToken("test_token", int(time.time()) + 3600)
+
+ # Mock credential that returns a token
+ credential = Mock(spec_set=["get_token"],
get_token=Mock(return_value=token))
+ policy = BearerTokenCredentialPolicy(credential, expected_scope)
+
+ # Initially jitter should be 0
+ assert policy._refresh_jitter == 0
+
+ # Mock random.randint to return a known value
+ with patch("azure.core.pipeline.policies._authentication.random.randint")
as mock_randint:
+ mock_randint.return_value = 42
+
+ # Request a token
+ policy._request_token(expected_scope)
+
+ # Verify jitter was set
+ assert policy._refresh_jitter == 42
+ mock_randint.assert_called_once_with(0, 60) #
MAX_REFRESH_JITTER_SECONDS
+
+ # Test that jitter is updated on subsequent token requests
+ with patch("azure.core.pipeline.policies._authentication.random.randint")
as mock_randint:
+ mock_randint.return_value = 25
+
+ # Request another token
+ policy._request_token(expected_scope)
+
+ # Verify jitter was updated
+ assert policy._refresh_jitter == 25
+ mock_randint.assert_called_once_with(0, 60)
def test_need_new_token_with_external_defined_token_class():
@@ -927,3 +1089,134 @@
# Verify the exception chaining
assert exc_info.value.__cause__ is not None
assert isinstance(exc_info.value.__cause__, HttpResponseError)
+
+
+def test_challenge_auth_header_stripped_after_redirect():
+ """Assuming the SensitiveHeaderCleanupPolicy is in the pipeline, the
authorization header should be stripped after
+ a redirect to a different domain by default, and preserved if the policy
is configured to disable cleanup."""
+
+ class MockTransport(HttpTransport):
+ def __init__(self, cleanup_disabled=False):
+ self._first = True
+ self._cleanup_disabled = cleanup_disabled
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+ def close(self):
+ pass
+
+ def open(self):
+ pass
+
+ def send(self, request, **kwargs):
+ if self._first:
+ self._first = False
+ assert request.headers["Authorization"] == "Bearer
{}".format(auth_header)
+ response = Response()
+ response.status_code = 307
+ response.headers["location"] =
"https://redirect-target.example.invalid"
+ return response
+
+ # Second request: after redirect
+ if self._cleanup_disabled:
+ assert request.headers.get("Authorization")
+ else:
+ assert not request.headers.get("Authorization")
+ response = Response()
+ response.status_code = 401
+ response.headers["WWW-Authenticate"] = (
+ 'Bearer error="insufficient_claims",
claims="eyJhY2Nlc3NfdG9rZW4iOnsiZm9vIjoiYmFyIn19"'
+ )
+ return response
+
+ auth_header = "token"
+ get_token_call_count = 0
+
+ def mock_get_token(*_, **__):
+ nonlocal get_token_call_count
+ get_token_call_count += 1
+ return AccessToken(auth_header, 0)
+
+ credential = Mock(spec_set=["get_token"], get_token=mock_get_token)
+ auth_policy = BearerTokenCredentialPolicy(credential, "scope")
+ redirect_policy = RedirectPolicy()
+ header_clean_up_policy = SensitiveHeaderCleanupPolicy()
+ pipeline = Pipeline(transport=MockTransport(), policies=[redirect_policy,
auth_policy, header_clean_up_policy])
+ response = pipeline.run(HttpRequest("GET", "https://legitimate.azure.com"))
+ assert response.http_response.status_code == 401
+
+ header_clean_up_policy =
SensitiveHeaderCleanupPolicy(disable_redirect_cleanup=True)
+ pipeline = Pipeline(
+ transport=MockTransport(cleanup_disabled=True),
policies=[redirect_policy, auth_policy, header_clean_up_policy]
+ )
+ response = pipeline.run(HttpRequest("GET", "https://legitimate.azure.com"))
+ assert response.http_response.status_code == 401
+
+
+def test_auth_header_stripped_after_cross_domain_redirect_with_retry():
+ """After a cross-domain redirect, if the redirected-to endpoint returns a
retryable status code,
+ the Authorization header should still be stripped on the retry attempt.
This verifies that the
+ insecure_domain_change flag persists across retries so
SensitiveHeaderCleanupPolicy continues to
+ remove the Authorization header."""
+
+ class MockTransport(HttpTransport):
+ def __init__(self):
+ self._request_count = 0
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+ def close(self):
+ pass
+
+ def open(self):
+ pass
+
+ def send(self, request, **kwargs):
+ self._request_count += 1
+
+ if self._request_count == 1:
+ # First request: to the original domain — should have auth
header
+ assert request.headers.get("Authorization") == "Bearer
{}".format(auth_header)
+ response = Response()
+ response.status_code = 307
+ response.headers["location"] =
"https://redirect-target.example.invalid"
+ return response
+
+ if self._request_count == 2:
+ # Second request: after redirect to attacker domain — auth
header should be stripped
+ assert not request.headers.get(
+ "Authorization"
+ ), "Authorization header should be stripped on first request
to redirected domain"
+ response = Response()
+ response.status_code = 500
+ return response
+
+ if self._request_count == 3:
+ # Third request: retry to attacker domain — auth header should
STILL be stripped
+ assert not request.headers.get(
+ "Authorization"
+ ), "Authorization header should be stripped on retry to
redirected domain"
+ response = Response()
+ response.status_code = 200
+ return response
+
+ raise RuntimeError("Unexpected request count:
{}".format(self._request_count))
+
+ auth_header = "token"
+ token = AccessToken(auth_header, 0)
+ credential = Mock(spec_set=["get_token"],
get_token=Mock(return_value=token))
+ auth_policy = BearerTokenCredentialPolicy(credential, "scope")
+ redirect_policy = RedirectPolicy()
+ retry_policy = RetryPolicy(retry_total=1, retry_backoff_factor=0)
+ header_clean_up_policy = SensitiveHeaderCleanupPolicy()
+ transport = MockTransport()
+ # Pipeline order matches the real default: redirect -> retry -> auth ->
... -> sensitive header cleanup
+ pipeline = Pipeline(
+ transport=transport,
+ policies=[redirect_policy, retry_policy, auth_policy,
header_clean_up_policy],
+ )
+ response = pipeline.run(HttpRequest("GET", "https://legitimate.azure.com"))
+ assert response.http_response.status_code == 200
+ assert transport._request_count == 3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.38.2/tests/test_pipeline.py
new/azure_core-1.38.3/tests/test_pipeline.py
--- old/azure_core-1.38.2/tests/test_pipeline.py 2026-02-18
03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/tests/test_pipeline.py 2026-03-12
20:09:29.000000000 +0100
@@ -225,6 +225,14 @@
assert formatted == "https://foo.core.windows.net/Tables?a=X&c=Y"
+def test_format_url_trailing_slash_preserved_with_query_only():
+ # Test that trailing slash in base URL is preserved when url_template is
query-string only
+ # https://github.com/Azure/azure-sdk-for-python/issues/45365
+ client = PipelineClientBase("{url}")
+ formatted = client.format_url("?versionid=2026-02-25",
url="https://storage.blob.core.windows.net/sample//a/a/")
+ assert formatted ==
"https://storage.blob.core.windows.net/sample//a/a/?versionid=2026-02-25"
+
+
def test_format_url_from_http_request():
client = PipelineClientBase("https://foo.core.windows.net")