Skip to content

Commit fe90b55

Browse files
Replace requests library default headers with python-sdk headers
1 parent e050984 commit fe90b55

7 files changed

Lines changed: 149 additions & 3 deletions

sdk/python/aleo/_client_common.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ def package_version() -> str:
6060
return "0.0.0"
6161

6262

63+
def user_agent() -> str:
64+
"""The SDK's ``User-Agent`` string, sent on every call.
65+
66+
Identifies the Python SDK (and its version) in the standard, always-logged
67+
header, overriding the underlying ``python-requests`` / ``python-httpx``
68+
default. Only injected on the default transport (see :func:`method_headers`
69+
and the scanners' header builders); when the caller supplies their own
70+
transport they own the headers, so the SDK does not set it.
71+
"""
72+
return f"aleo-python-sdk/{package_version()}"
73+
74+
6375
def make_default_headers() -> dict[str, str]:
6476
return {
6577
"X-Aleo-SDK-Version": package_version(),
@@ -79,7 +91,7 @@ def method_headers(
7991
) -> dict[str, str]:
8092
if has_custom_transport:
8193
return user_headers(headers)
82-
return {**headers, "X-ALEO-METHOD": method}
94+
return {**headers, "X-ALEO-METHOD": method, "User-Agent": user_agent()}
8395

8496

8597
def jwt_origin(host: str) -> str:

sdk/python/aleo/async_record_scanner.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Any
77
from urllib.parse import urlparse
88

9-
from ._client_common import jwt_expired
9+
from ._client_common import jwt_expired, user_agent
1010
from ._scanner_common import (
1111
DecryptionNotEnabledError,
1212
OwnedFilter,
@@ -104,6 +104,7 @@ def __init__(
104104
self._account: Any | None = None
105105

106106
# Build httpx client
107+
self._has_custom_transport: bool = transport is not None
107108
if transport is not None:
108109
self._client: Any = httpx.AsyncClient(transport=transport)
109110
else:
@@ -174,6 +175,11 @@ def set_account(self, account: Any) -> None:
174175
async def _build_headers(self) -> dict[str, str]:
175176
"""Build authentication headers, refreshing JWT if needed."""
176177
hdrs: dict[str, str] = {"Content-Type": "application/json"}
178+
# Identify the SDK on every call — unless a custom transport owns the
179+
# HTTP layer, in which case the caller controls headers.
180+
sdk_ua = None if self._has_custom_transport else user_agent()
181+
if sdk_ua:
182+
hdrs["User-Agent"] = sdk_ua
177183

178184
if self._api_key:
179185
hdrs[self._api_key["header"]] = self._api_key["value"]
@@ -183,6 +189,8 @@ async def _build_headers(self) -> dict[str, str]:
183189
if self._api_key and self.consumer_id:
184190
jwt_url = f"{self._origin}/jwts/{self.consumer_id}"
185191
jwt_hdrs = {self._api_key["header"]: self._api_key["value"]}
192+
if sdk_ua:
193+
jwt_hdrs["User-Agent"] = sdk_ua
186194
resp = await self._client.post(jwt_url, headers=jwt_hdrs)
187195
if resp.is_success:
188196
auth = resp.headers.get("Authorization") or resp.headers.get("authorization")

sdk/python/aleo/record_scanner.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import requests
99

10-
from ._client_common import jwt_expired
10+
from ._client_common import jwt_expired, user_agent
1111
from ._scanner_common import (
1212
DecryptionNotEnabledError,
1313
OwnedFilter,
@@ -120,6 +120,11 @@ def _http(self, method: str, url: str, **kwargs: Any) -> requests.Response:
120120
def _build_headers(self) -> dict[str, str]:
121121
"""Build authentication headers, refreshing JWT if needed."""
122122
hdrs: dict[str, str] = {"Content-Type": "application/json"}
123+
# Identify the SDK on every call — unless a custom transport owns the
124+
# HTTP layer, in which case the caller controls headers.
125+
sdk_ua = None if callable(self._transport) else user_agent()
126+
if sdk_ua:
127+
hdrs["User-Agent"] = sdk_ua
123128

124129
# Always attach api_key header if set
125130
if self._api_key:
@@ -132,6 +137,8 @@ def _build_headers(self) -> dict[str, str]:
132137
# Refresh JWT
133138
jwt_url = f"{self._origin}/jwts/{self.consumer_id}"
134139
jwt_hdrs = {self._api_key["header"]: self._api_key["value"]}
140+
if sdk_ua:
141+
jwt_hdrs["User-Agent"] = sdk_ua
135142
resp = self._http("POST", jwt_url, headers=jwt_hdrs)
136143
if resp.ok:
137144
auth = resp.headers.get("Authorization") or resp.headers.get("authorization")

sdk/python/tests/test_network_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ def test_default_sdk_headers_present() -> None:
233233
assert "X-Aleo-SDK-Version" in req_headers
234234
assert "X-Aleo-environment" in req_headers
235235
assert req_headers["X-Aleo-environment"] == "python"
236+
# The standard User-Agent identifies the SDK (overriding requests' default).
237+
from aleo._client_common import package_version
238+
assert req_headers["User-Agent"] == f"aleo-python-sdk/{package_version()}"
236239

237240

238241
@resp_lib.activate
@@ -244,6 +247,11 @@ def test_per_method_header() -> None:
244247
assert req_headers.get("X-ALEO-METHOD") == "getBlock"
245248

246249

250+
def test_user_agent_value() -> None:
251+
from aleo._client_common import package_version, user_agent
252+
assert user_agent() == f"aleo-python-sdk/{package_version()}"
253+
254+
247255
def test_custom_transport_callable_used_for_requests() -> None:
248256
"""A callable transport is invoked for every HTTP request."""
249257
import requests as _requests
@@ -273,6 +281,30 @@ def test_custom_transport_suppresses_sdk_headers() -> None:
273281
req_headers = resp_lib.calls[0].request.headers
274282
assert "X-Aleo-SDK-Version" not in req_headers
275283
assert "X-ALEO-METHOD" not in req_headers
284+
# UA is suppressed under a custom transport: the SDK does not set its own
285+
# (requests' own default python-requests/... may still be present).
286+
assert not req_headers.get("User-Agent", "").startswith("aleo-python-sdk/")
287+
288+
289+
def test_custom_transport_preserves_user_supplied_user_agent() -> None:
290+
"""Under a custom transport the caller owns headers — a User-Agent they set
291+
is passed through untouched (not stripped as an SDK header)."""
292+
import requests as _requests
293+
294+
captured: dict[str, Any] = {}
295+
296+
def my_transport(method: str, url: str, **kwargs: Any) -> _requests.Response:
297+
captured["headers"] = dict(kwargs.get("headers") or {})
298+
r = _requests.Response()
299+
r.status_code = 200
300+
r._content = b"{}"
301+
return r
302+
303+
c = AleoNetworkClient(
304+
BASE, network=NET, transport=my_transport, headers={"User-Agent": "myapp/1.0"}
305+
)
306+
c.get_latest_block()
307+
assert captured["headers"].get("User-Agent") == "myapp/1.0"
276308

277309

278310
@resp_lib.activate

sdk/python/tests/test_network_client_async.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ def handler(req: httpx.Request) -> httpx.Response:
249249
await c.get_latest_block()
250250
assert "X-Aleo-SDK-Version" in captured[0].headers
251251
assert captured[0].headers.get("X-Aleo-environment") == "python"
252+
from aleo._client_common import package_version
253+
assert captured[0].headers.get("User-Agent") == f"aleo-python-sdk/{package_version()}"
252254

253255

254256
@pytest.mark.asyncio

sdk/python/tests/test_record_scanner.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,43 @@ def test_find_record_returns_first() -> None:
662662

663663
record = scanner.find_record({"uuid": GOLDEN_UUID}) # type: ignore[arg-type]
664664
assert record == OWNED_RECORDS[0]
665+
666+
667+
# ---------------------------------------------------------------------------
668+
# User-Agent header
669+
# ---------------------------------------------------------------------------
670+
671+
@resp_lib.activate
672+
def test_scanner_sends_user_agent() -> None:
673+
"""Every scanner call carries the SDK User-Agent."""
674+
from aleo.mainnet import Field
675+
from aleo._client_common import package_version
676+
677+
resp_lib.add(resp_lib.POST, f"{HOST}/records/owned", json=[])
678+
scanner = _make_scanner()
679+
scanner._uuid = Field.from_string(GOLDEN_UUID)
680+
scanner.owned({"uuid": GOLDEN_UUID, "unspent": True}) # type: ignore[arg-type]
681+
682+
hdrs = resp_lib.calls[0].request.headers
683+
assert hdrs["User-Agent"] == f"aleo-python-sdk/{package_version()}"
684+
685+
686+
def test_scanner_custom_transport_suppresses_user_agent() -> None:
687+
"""A custom transport owns the HTTP layer — the SDK sets no User-Agent."""
688+
import requests as _requests
689+
from aleo.mainnet import Field
690+
691+
captured: dict[str, Any] = {}
692+
693+
def transport(method: str, url: str, **kwargs: Any) -> _requests.Response:
694+
captured["headers"] = dict(kwargs.get("headers") or {})
695+
r = _requests.Response()
696+
r.status_code = 200
697+
r._content = b"[]"
698+
return r
699+
700+
scanner = _make_scanner(transport=transport)
701+
scanner._uuid = Field.from_string(GOLDEN_UUID)
702+
scanner.owned({"uuid": GOLDEN_UUID, "unspent": True}) # type: ignore[arg-type]
703+
704+
assert "User-Agent" not in captured["headers"]

sdk/python/tests/test_record_scanner_async.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,48 @@ async def test_async_find_credits_records_success() -> None:
429429

430430
result3 = await scanner3.find_credits_records([999], {}) # type: ignore[arg-type]
431431
assert result3 == []
432+
433+
434+
# ---------------------------------------------------------------------------
435+
# User-Agent header
436+
# ---------------------------------------------------------------------------
437+
438+
@pytest.mark.asyncio
439+
async def test_async_scanner_sends_user_agent() -> None:
440+
"""Async scanner carries the SDK User-Agent when it owns the transport."""
441+
from aleo.mainnet import Field
442+
from aleo._client_common import package_version
443+
444+
captured: dict[str, Any] = {}
445+
446+
def handler(request: httpx.Request) -> httpx.Response:
447+
captured["ua"] = request.headers.get("user-agent")
448+
return httpx.Response(200, json=[])
449+
450+
# No transport passed to __init__ (so it is NOT treated as a custom
451+
# transport); swap the client for a mock one, mirroring the network-client
452+
# test pattern.
453+
scanner = AsyncRecordScanner(BASE_URL)
454+
scanner._client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
455+
scanner._uuid = Field.from_string(GOLDEN_UUID)
456+
await scanner.owned({"uuid": GOLDEN_UUID, "unspent": True}) # type: ignore[arg-type]
457+
458+
assert captured["ua"] == f"aleo-python-sdk/{package_version()}"
459+
460+
461+
@pytest.mark.asyncio
462+
async def test_async_scanner_custom_transport_suppresses_user_agent() -> None:
463+
"""A custom transport (passed to __init__) suppresses the SDK User-Agent."""
464+
from aleo.mainnet import Field
465+
466+
captured: dict[str, Any] = {}
467+
468+
def handler(request: httpx.Request) -> httpx.Response:
469+
captured["ua"] = request.headers.get("user-agent")
470+
return httpx.Response(200, json=[])
471+
472+
scanner = AsyncRecordScanner(BASE_URL, transport=httpx.MockTransport(handler))
473+
scanner._uuid = Field.from_string(GOLDEN_UUID)
474+
await scanner.owned({"uuid": GOLDEN_UUID, "unspent": True}) # type: ignore[arg-type]
475+
476+
assert not (captured["ua"] or "").startswith("aleo-python-sdk/")

0 commit comments

Comments
 (0)