From 8cf83cd2ad8d2ab0ed7d99bc48835085bb1f51ac Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Mon, 22 Jun 2026 15:53:54 -0700 Subject: [PATCH 1/5] Defer asyncio import to AIOHTTPClient instantiation asyncio was imported at module level in _http_client.py but only used by AIOHTTPClient.sleep_async(). Since _http_client is loaded eagerly by stripe/__init__.py, every user paid ~1.8s on CPU-constrained environments (e.g. Lambda cold starts) for a module they likely never use. Move the import into AIOHTTPClient.__init__ so only users with aiohttp installed incur the cost. Co-Authored-By: Claude Sonnet 4.6 Committed-By-Agent: claude --- stripe/_http_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stripe/_http_client.py b/stripe/_http_client.py index b09b4984f..761ad3e76 100644 --- a/stripe/_http_client.py +++ b/stripe/_http_client.py @@ -5,7 +5,6 @@ import random import threading import json -import asyncio import ssl from http.client import HTTPResponse @@ -1409,6 +1408,10 @@ def __init__( self.aiohttp = _lib + import asyncio + + self._asyncio = asyncio + self._timeout = timeout self._user_session = session self._user_connector = connector @@ -1441,7 +1444,7 @@ def _session(self): return self._cached_session def sleep_async(self, secs): - return asyncio.sleep(secs) + return self._asyncio.sleep(secs) def request(self) -> Tuple[bytes, int, Mapping[str, str]]: raise NotImplementedError( From f81e5759f8aeac4159fd4da804429e10c8c033d8 Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Mon, 22 Jun 2026 17:56:16 -0700 Subject: [PATCH 2/5] Replace uuid.uuid4() with os.urandom(16).hex() for idempotency keys The uuid module pulls in 20 transitive modules (including re, enum, platform) and takes 300-700ms to import under Lambda-like CPU constraints. Since os is already imported in this file and os.urandom(16).hex() produces an equivalent 32-char hex string with cryptographic randomness, we can drop the uuid dependency entirely. Co-Authored-By: Claude Sonnet 4.6 Committed-By-Agent: claude --- stripe/_api_requestor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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": From 38ef34d33c1c6b8f09286621b1a80458785dcb5c Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Mon, 22 Jun 2026 18:04:40 -0700 Subject: [PATCH 3/5] Move HTTP client library detection to module load time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of detecting and importing HTTP libraries (requests, httpx, etc.) inside StripeClient.__init__, resolve them once at module load time. This shifts the expensive import cost from the invoke phase (3s Lambda timeout) to the init phase (10s budget). The resolved class is stored directly — new_default_http_client() and new_http_client_async_fallback() just instantiate it. Adding a new client means defining the class and adding one entry to the resolution cascade at the bottom of the file. Co-Authored-By: Claude Sonnet 4.6 Committed-By-Agent: claude --- stripe/_http_client.py | 113 ++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 52 deletions(-) diff --git a/stripe/_http_client.py b/stripe/_http_client.py index 761ad3e76..4049bea25 100644 --- a/stripe/_http_client.py +++ b/stripe/_http_client.py @@ -64,61 +64,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): @@ -1553,3 +1503,62 @@ 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). +# +# 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() From 759d95a636598983e03497b29e7c6fbcb5647a1f Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Mon, 22 Jun 2026 18:04:45 -0700 Subject: [PATCH 4/5] Revert "Defer asyncio import to AIOHTTPClient instantiation" This reverts commit 8cf83cd2ad8d2ab0ed7d99bc48835085bb1f51ac. --- stripe/_http_client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/stripe/_http_client.py b/stripe/_http_client.py index 4049bea25..12b478dea 100644 --- a/stripe/_http_client.py +++ b/stripe/_http_client.py @@ -5,6 +5,7 @@ import random import threading import json +import asyncio import ssl from http.client import HTTPResponse @@ -1358,10 +1359,6 @@ def __init__( self.aiohttp = _lib - import asyncio - - self._asyncio = asyncio - self._timeout = timeout self._user_session = session self._user_connector = connector @@ -1394,7 +1391,7 @@ def _session(self): return self._cached_session def sleep_async(self, secs): - return self._asyncio.sleep(secs) + return asyncio.sleep(secs) def request(self) -> Tuple[bytes, int, Mapping[str, str]]: raise NotImplementedError( From bc2901ac791c1ee781bea5e60c2c2df9e16d0b84 Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Mon, 22 Jun 2026 18:42:24 -0700 Subject: [PATCH 5/5] Fix import detection tests and restore precedence docs Update TestImports to call _resolve_sync_client() and _resolve_async_client() directly, since detection now happens at module load time and patching builtins.__import__ after the fact has no effect on the pre-resolved class. Restore the client precedence documentation as a comment block above the resolution cascade. Co-Authored-By: Claude Sonnet 4.6 Committed-By-Agent: claude --- stripe/_http_client.py | 12 ++++++++++++ tests/test_http_client.py | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/stripe/_http_client.py b/stripe/_http_client.py index 12b478dea..96fc78d68 100644 --- a/stripe/_http_client.py +++ b/stripe/_http_client.py @@ -1509,10 +1509,22 @@ async def close_async(self): # 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 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]