diff --git a/stripe/_api_requestor.py b/stripe/_api_requestor.py index 514c8ba82..6eb5468b1 100644 --- a/stripe/_api_requestor.py +++ b/stripe/_api_requestor.py @@ -24,7 +24,6 @@ NoReturn, Unpack, ) -import uuid from urllib.parse import urlsplit, urlunsplit, parse_qs # breaking circular dependency @@ -580,7 +579,7 @@ def request_headers( # IKs should be set for all POST requests and v2 delete requests if method == "post" or (api_mode == "V2" and method == "delete"): - headers.setdefault("Idempotency-Key", str(uuid.uuid4())) + headers.setdefault("Idempotency-Key", os.urandom(16).hex()) if method == "post": if api_mode == "V2": diff --git a/stripe/_http_client.py b/stripe/_http_client.py index b09b4984f..96fc78d68 100644 --- a/stripe/_http_client.py +++ b/stripe/_http_client.py @@ -65,61 +65,11 @@ def _now_ms(): def new_default_http_client(*args: Any, **kwargs: Any) -> "HTTPClient": - """ - This method creates and returns a new HTTPClient based on what libraries are available. It uses the following precedence rules: - - 1. Urlfetch (this is provided by Google App Engine, so if it's present you probably want it) - 2. Requests (popular library, the top priority for all environments outside Google App Engine, but not always present) - 3. Pycurl (another library, not always present, not as preferred as Requests but at least it verifies SSL certs) - 4. urllib with a warning (basically always present, a reasonable final default) - - For performance, it only imports what it's actually going to use. But, it re-calculates every time its called, so probably save its result instead of calling it multiple times. - """ - try: - from google.appengine.api import urlfetch # type: ignore # noqa: F401 - except ImportError: - pass - else: - return UrlFetchClient(*args, **kwargs) - - try: - import requests # noqa: F401 - except ImportError: - pass - else: - return RequestsClient(*args, **kwargs) - - try: - import pycurl # type: ignore # noqa: F401 - except ImportError: - pass - else: - return PycurlClient(*args, **kwargs) - - return UrllibClient(*args, **kwargs) + return _default_sync_client(*args, **kwargs) def new_http_client_async_fallback(*args: Any, **kwargs: Any) -> "HTTPClient": - """ - Similar to `new_default_http_client` above, this returns a client that can handle async HTTP requests, if available. - """ - - try: - import httpx # noqa: F401 - import anyio # noqa: F401 - except ImportError: - pass - else: - return HTTPXClient(*args, **kwargs) - - try: - import aiohttp # noqa: F401 - except ImportError: - pass - else: - return AIOHTTPClient(*args, **kwargs) - - return NoImportFoundAsyncClient(*args, **kwargs) + return _default_async_client(*args, **kwargs) class HTTPClient(object): @@ -1550,3 +1500,74 @@ async def request_stream_async( async def close_async(self): self.raise_async_client_import_error() + + +# --- Client resolution --- +# Detect available HTTP libraries at module load time so the expensive imports +# (e.g. requests, httpx) happen during Python's init phase rather than when +# StripeClient() is constructed. This matters in environments like AWS Lambda +# where module loading has a generous timeout (10s) but handler invocation +# does not (often 3s). +# +# Sync client precedence: +# 1. Urlfetch (Google App Engine — if present, you probably want it) +# 2. Requests (popular, top priority outside GAE) +# 3. Pycurl (verifies SSL certs, but less preferred than Requests) +# 4. urllib (stdlib fallback, basically always present) +# +# Async client precedence: +# 1. httpx + anyio (both required) +# 2. aiohttp +# 3. NoImportFoundAsyncClient (raises on use) +# +# To add a new client: define the class above, then add it to the appropriate +# cascade below. The resolved class is stored directly — new_default_http_client() +# and new_http_client_async_fallback() just call it. + + +def _resolve_sync_client(): + try: + from google.appengine.api import urlfetch # type: ignore # noqa: F401 + + return UrlFetchClient + except ImportError: + pass + + try: + import requests # noqa: F401 + + return RequestsClient + except ImportError: + pass + + try: + import pycurl # type: ignore # noqa: F401 + + return PycurlClient + except ImportError: + pass + + return UrllibClient + + +def _resolve_async_client(): + try: + import httpx # noqa: F401 + import anyio # noqa: F401 + + return HTTPXClient + except ImportError: + pass + + try: + import aiohttp # noqa: F401 + + return AIOHTTPClient + except ImportError: + pass + + return NoImportFoundAsyncClient + + +_default_sync_client = _resolve_sync_client() +_default_async_client = _resolve_async_client() diff --git a/tests/test_http_client.py b/tests/test_http_client.py index be0f8400f..ea74cf7d9 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -101,8 +101,8 @@ def test_default_httpclient_from_imports( with patch("builtins.__import__") as mocked_import_fn: mocked_import_fn.side_effect = mock_import(available_libs) - client = _http_client.new_default_http_client() - assert isinstance(client, expected) + resolved_class = _http_client._resolve_sync_client() + assert resolved_class is expected @pytest.mark.parametrize( ["available_libs", "expected"], @@ -123,8 +123,8 @@ def test_default_async_httpclient_from_imports( with patch("builtins.__import__") as mocked_import_fn: mocked_import_fn.side_effect = mock_import(available_libs) - client = _http_client.new_http_client_async_fallback() - assert isinstance(client, expected) + resolved_class = _http_client._resolve_async_client() + assert resolved_class is expected MakeReqFunc = Callable[[str, str, Dict[str, str], Optional[str]], Any]