Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions stripe/_api_requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
NoReturn,
Unpack,
)
import uuid
from urllib.parse import urlsplit, urlunsplit, parse_qs

# breaking circular dependency
Expand Down Expand Up @@ -100,6 +99,11 @@ def is_v2_delete_resp(method: str, api_mode: ApiMode) -> bool:
return method == "delete" and api_mode == "V2"


def _generate_idempotency_key() -> str:
b = os.urandom(16)
return f"{b[0:4].hex()}-{b[4:6].hex()}-{b[6:8].hex()}-{b[8:10].hex()}-{b[10:].hex()}"


class _APIRequestor(object):
_instance: ClassVar["_APIRequestor|None"] = None

Expand Down Expand Up @@ -580,7 +584,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", _generate_idempotency_key())

if method == "post":
if api_mode == "V2":
Expand Down
127 changes: 75 additions & 52 deletions stripe/_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -1550,3 +1500,76 @@ 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:
Comment thread
jar-stripe marked this conversation as resolved.
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


# Called at module load time so HTTP library imports happen during Python's init
# phase rather than inside StripeClient.__init__().
_default_sync_client = _resolve_sync_client()
Comment thread
jar-stripe marked this conversation as resolved.
_default_async_client = _resolve_async_client()
8 changes: 4 additions & 4 deletions tests/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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]
Expand Down
Loading