From 84cb686e3d12d4ad46aa52ca4f3dcdac403bfd22 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Tue, 2 Jun 2026 17:10:06 -0700 Subject: [PATCH] Add source field to user-agent header --- stripe/_api_requestor.py | 19 +++++++++++++++++++ tests/test_api_requestor.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/stripe/_api_requestor.py b/stripe/_api_requestor.py index 10bdebdcc..514c8ba82 100644 --- a/stripe/_api_requestor.py +++ b/stripe/_api_requestor.py @@ -1,7 +1,10 @@ from io import BytesIO, IOBase +import functools +import hashlib import json import os import platform +import socket from typing import ( Any, AsyncIterable, @@ -72,6 +75,19 @@ _default_proxy: Optional[str] = None +@functools.lru_cache(maxsize=None) +def _get_uname_hash() -> Optional[str]: + try: + parts: List[str] = list(platform.uname()) + try: + parts.append(socket.gethostname()) + except Exception: + pass + return hashlib.md5(" ".join(parts).encode()).hexdigest() + except Exception: + return None + + def _maybe_emit_stripe_notice(rheaders: Mapping[str, str]) -> None: notice = rheaders.get("Stripe-Notice") if notice: @@ -525,6 +541,9 @@ def request_headers( "lang": "python", "httplib": self._get_http_client().name, } + uname_hash = _get_uname_hash() + if uname_hash is not None: + ua["source"] = uname_hash attr_funcs: List[Tuple[str, Callable[[], str]]] = [ ("lang_version", platform.python_version), ] diff --git a/tests/test_api_requestor.py b/tests/test_api_requestor.py index ff3530359..326be0bb9 100644 --- a/tests/test_api_requestor.py +++ b/tests/test_api_requestor.py @@ -1,5 +1,6 @@ import datetime import json +import re import tempfile import uuid from collections import OrderedDict @@ -788,6 +789,36 @@ def fail(): last_call.get_raw_header("X-Stripe-Client-User-Agent") ) + def test_source_field_is_md5_hex(self, requestor, http_client_mock): + http_client_mock.stub_request( + "get", path=self.v1_path, rbody="{}", rcode=200 + ) + requestor.request("get", self.v1_path, {}, base_address="api") + + last_call = http_client_mock.get_last_call() + client_ua = json.loads( + last_call.get_raw_header("X-Stripe-Client-User-Agent") + ) + assert "source" in client_ua + assert re.fullmatch(r"[0-9a-f]{32}", client_ua["source"]) + + def test_source_field_absent_when_uname_fails( + self, requestor, mocker, http_client_mock + ): + http_client_mock.stub_request( + "get", path=self.v1_path, rbody="{}", rcode=200 + ) + mocker.patch( + "stripe._api_requestor._get_uname_hash", return_value=None + ) + requestor.request("get", self.v1_path, {}, base_address="api") + + last_call = http_client_mock.get_last_call() + client_ua = json.loads( + last_call.get_raw_header("X-Stripe-Client-User-Agent") + ) + assert "source" not in client_ua + def test_uses_given_idempotency_key(self, requestor, http_client_mock): method = "post" http_client_mock.stub_request(