Files
Hotel-Booking/Backend/venv/lib/python3.12/site-packages/stripe/_http_client.py
Iliyan Angelov 0c59fe1173 updates
2025-11-17 18:26:30 +02:00

1547 lines
49 KiB
Python

from io import BytesIO
import textwrap
import email
import time
import random
import threading
import json
import asyncio
import ssl
from http.client import HTTPResponse
# Used for global variables
import stripe # noqa: IMP101
from stripe import _util
from stripe._request_metrics import RequestMetrics
from stripe._error import APIConnectionError
from typing import (
Any,
Dict,
Iterable,
List,
Mapping,
MutableMapping,
Optional,
Tuple,
ClassVar,
Union,
cast,
overload,
AsyncIterable,
)
from typing_extensions import (
TYPE_CHECKING,
Literal,
NoReturn,
TypedDict,
Awaitable,
Never,
)
if TYPE_CHECKING:
from urllib.parse import ParseResult
try:
from requests import Session as RequestsSession
except ImportError:
pass
try:
from httpx import Timeout as HTTPXTimeout
from httpx import Client as HTTPXClientType
except ImportError:
pass
try:
from aiohttp import ClientTimeout as AIOHTTPTimeout
from aiohttp import StreamReader as AIOHTTPStreamReader
except ImportError:
pass
def _now_ms():
return int(round(time.time() * 1000))
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)
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)
class HTTPClient(object):
"""
Base HTTP client that custom clients can inherit from.
"""
name: ClassVar[str]
class _Proxy(TypedDict):
http: Optional[str]
https: Optional[str]
MAX_DELAY = 5
INITIAL_DELAY = 0.5
MAX_RETRY_AFTER = 60
_proxy: Optional[_Proxy]
_verify_ssl_certs: bool
def __init__(
self,
verify_ssl_certs: bool = True,
proxy: Optional[Union[str, _Proxy]] = None,
async_fallback_client: Optional["HTTPClient"] = None,
_lib=None, # used for internal unit testing
):
self._verify_ssl_certs = verify_ssl_certs
if proxy:
if isinstance(proxy, str):
proxy = {"http": proxy, "https": proxy}
if not isinstance(proxy, dict): # pyright: ignore[reportUnnecessaryIsInstance]
raise ValueError(
"Proxy(ies) must be specified as either a string "
"URL or a dict() with string URL under the"
" "
"https"
" and/or "
"http"
" keys."
)
self._proxy = proxy.copy() if proxy else None
self._async_fallback_client = async_fallback_client
self._thread_local = threading.local()
def _should_retry(
self,
response: Optional[Tuple[Any, int, Optional[Mapping[str, str]]]],
api_connection_error: Optional[APIConnectionError],
num_retries: int,
max_network_retries: Optional[int],
):
max_network_retries = (
max_network_retries if max_network_retries is not None else 0
)
if num_retries >= max_network_retries:
return False
if response is None:
# We generally want to retry on timeout and connection
# exceptions, but defer this decision to underlying subclass
# implementations. They should evaluate the driver-specific
# errors worthy of retries, and set flag on the error returned.
assert api_connection_error is not None
return api_connection_error.should_retry
_, status_code, rheaders = response
# The API may ask us not to retry (eg; if doing so would be a no-op)
# or advise us to retry (eg; in cases of lock timeouts); we defer to that.
#
# Note that we expect the headers object to be a CaseInsensitiveDict, as is the case with the requests library.
if rheaders is not None and "stripe-should-retry" in rheaders:
if rheaders["stripe-should-retry"] == "false":
return False
if rheaders["stripe-should-retry"] == "true":
return True
# Retry on conflict errors.
if status_code == 409:
return True
# Retry on 500, 503, and other internal errors.
#
# Note that we expect the stripe-should-retry header to be false
# in most cases when a 500 is returned, since our idempotency framework
# would typically replay it anyway.
if status_code >= 500:
return True
return False
def _retry_after_header(
self, response: Optional[Tuple[Any, Any, Mapping[str, str]]] = None
):
if response is None:
return None
_, _, rheaders = response
try:
return int(rheaders["retry-after"])
except (KeyError, ValueError):
return None
def _sleep_time_seconds(
self,
num_retries: int,
response: Optional[Tuple[Any, Any, Mapping[str, str]]] = None,
) -> float:
"""
Apply exponential backoff with initial_network_retry_delay on the number of num_retries so far as inputs.
Do not allow the number to exceed `max_network_retry_delay`.
"""
sleep_seconds = min(
HTTPClient.INITIAL_DELAY * (2 ** (num_retries - 1)),
HTTPClient.MAX_DELAY,
)
sleep_seconds = self._add_jitter_time(sleep_seconds)
# But never sleep less than the base sleep seconds.
sleep_seconds = max(HTTPClient.INITIAL_DELAY, sleep_seconds)
# And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask.
retry_after = self._retry_after_header(response) or 0
if retry_after <= HTTPClient.MAX_RETRY_AFTER:
sleep_seconds = max(retry_after, sleep_seconds)
return sleep_seconds
def _add_jitter_time(self, sleep_seconds: float) -> float:
"""
Randomize the value in `[(sleep_seconds/ 2) to (sleep_seconds)]`.
Also separated method here to isolate randomness for tests
"""
sleep_seconds *= 0.5 * (1 + random.uniform(0, 1))
return sleep_seconds
def _add_telemetry_header(
self, headers: Mapping[str, str]
) -> Mapping[str, str]:
last_request_metrics = getattr(
self._thread_local, "last_request_metrics", None
)
if stripe.enable_telemetry and last_request_metrics:
telemetry = {
"last_request_metrics": last_request_metrics.payload()
}
ret = dict(headers)
ret["X-Stripe-Client-Telemetry"] = json.dumps(telemetry)
return ret
return headers
def _record_request_metrics(self, response, request_start, usage):
_, _, rheaders = response
if "Request-Id" in rheaders and stripe.enable_telemetry:
request_id = rheaders["Request-Id"]
request_duration_ms = _now_ms() - request_start
self._thread_local.last_request_metrics = RequestMetrics(
request_id, request_duration_ms, usage=usage
)
def request_with_retries(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data: Any = None,
max_network_retries: Optional[int] = None,
*,
_usage: Optional[List[str]] = None,
) -> Tuple[str, int, Mapping[str, str]]:
return self._request_with_retries_internal(
method,
url,
headers,
post_data,
is_streaming=False,
max_network_retries=max_network_retries,
_usage=_usage,
)
def request_stream_with_retries(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data=None,
max_network_retries=None,
*,
_usage: Optional[List[str]] = None,
) -> Tuple[Any, int, Mapping[str, str]]:
return self._request_with_retries_internal(
method,
url,
headers,
post_data,
is_streaming=True,
max_network_retries=max_network_retries,
_usage=_usage,
)
def _request_with_retries_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data: Any,
is_streaming: bool,
max_network_retries: Optional[int],
*,
_usage: Optional[List[str]] = None,
) -> Tuple[Any, int, Mapping[str, str]]:
headers = self._add_telemetry_header(headers)
num_retries = 0
while True:
request_start = _now_ms()
try:
if is_streaming:
response = self.request_stream(
method, url, headers, post_data
)
else:
response = self.request(method, url, headers, post_data)
connection_error = None
except APIConnectionError as e:
connection_error = e
response = None
if self._should_retry(
response, connection_error, num_retries, max_network_retries
):
if connection_error:
_util.log_info(
"Encountered a retryable error %s"
% connection_error.user_message
)
num_retries += 1
sleep_time = self._sleep_time_seconds(num_retries, response)
_util.log_info(
(
"Initiating retry %i for request %s %s after "
"sleeping %.2f seconds."
% (num_retries, method, url, sleep_time)
)
)
time.sleep(sleep_time)
else:
if response is not None:
self._record_request_metrics(
response, request_start, usage=_usage
)
return response
else:
assert connection_error is not None
raise connection_error
def request(
self,
method: str,
url: str,
headers: Optional[Mapping[str, str]],
post_data: Any = None,
*,
_usage: Optional[List[str]] = None,
) -> Tuple[str, int, Mapping[str, str]]:
raise NotImplementedError(
"HTTPClient subclasses must implement `request`"
)
def request_stream(
self,
method: str,
url: str,
headers: Optional[Mapping[str, str]],
post_data: Any = None,
*,
_usage: Optional[List[str]] = None,
) -> Tuple[Any, int, Mapping[str, str]]:
raise NotImplementedError(
"HTTPClient subclasses must implement `request_stream`"
)
def close(self):
raise NotImplementedError(
"HTTPClient subclasses must implement `close`"
)
async def request_with_retries_async(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data=None,
max_network_retries: Optional[int] = None,
*,
_usage: Optional[List[str]] = None,
) -> Tuple[Any, int, Any]:
return await self._request_with_retries_internal_async(
method,
url,
headers,
post_data,
is_streaming=False,
max_network_retries=max_network_retries,
_usage=_usage,
)
async def request_stream_with_retries_async(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data=None,
max_network_retries=None,
*,
_usage: Optional[List[str]] = None,
) -> Tuple[AsyncIterable[bytes], int, Any]:
return await self._request_with_retries_internal_async(
method,
url,
headers,
post_data,
is_streaming=True,
max_network_retries=max_network_retries,
_usage=_usage,
)
@overload
async def _request_with_retries_internal_async(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: Literal[False],
max_network_retries: Optional[int],
*,
_usage: Optional[List[str]] = None,
) -> Tuple[Any, int, Mapping[str, str]]: ...
@overload
async def _request_with_retries_internal_async(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: Literal[True],
max_network_retries: Optional[int],
*,
_usage: Optional[List[str]] = None,
) -> Tuple[AsyncIterable[bytes], int, Mapping[str, str]]: ...
async def _request_with_retries_internal_async(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: bool,
max_network_retries: Optional[int],
*,
_usage: Optional[List[str]] = None,
) -> Tuple[Any, int, Mapping[str, str]]:
headers = self._add_telemetry_header(headers)
num_retries = 0
while True:
request_start = _now_ms()
try:
if is_streaming:
response = await self.request_stream_async(
method, url, headers, post_data
)
else:
response = await self.request_async(
method, url, headers, post_data
)
connection_error = None
except APIConnectionError as e:
connection_error = e
response = None
if self._should_retry(
response, connection_error, num_retries, max_network_retries
):
if connection_error:
_util.log_info(
"Encountered a retryable error %s"
% connection_error.user_message
)
num_retries += 1
sleep_time = self._sleep_time_seconds(num_retries, response)
_util.log_info(
(
"Initiating retry %i for request %s %s after "
"sleeping %.2f seconds."
% (num_retries, method, url, sleep_time)
)
)
await self.sleep_async(sleep_time)
else:
if response is not None:
self._record_request_metrics(
response, request_start, usage=_usage
)
return response
else:
assert connection_error is not None
raise connection_error
async def request_async(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[bytes, int, Mapping[str, str]]:
if self._async_fallback_client is not None:
return await self._async_fallback_client.request_async(
method, url, headers, post_data
)
raise NotImplementedError(
"HTTPClient subclasses must implement `request_async`"
)
async def request_stream_async(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[AsyncIterable[bytes], int, Mapping[str, str]]:
if self._async_fallback_client is not None:
return await self._async_fallback_client.request_stream_async(
method, url, headers, post_data
)
raise NotImplementedError(
"HTTPClient subclasses must implement `request_stream_async`"
)
async def close_async(self):
if self._async_fallback_client is not None:
return await self._async_fallback_client.close_async()
raise NotImplementedError(
"HTTPClient subclasses must implement `close_async`"
)
def sleep_async(self, secs: float) -> Awaitable[None]:
if self._async_fallback_client is not None:
return self._async_fallback_client.sleep_async(secs)
raise NotImplementedError(
"HTTPClient subclasses must implement `sleep`"
)
class RequestsClient(HTTPClient):
name = "requests"
def __init__(
self,
timeout: Union[float, Tuple[float, float]] = 80,
session: Optional["RequestsSession"] = None,
verify_ssl_certs: bool = True,
proxy: Optional[Union[str, HTTPClient._Proxy]] = None,
async_fallback_client: Optional[HTTPClient] = None,
_lib=None, # used for internal unit testing
**kwargs,
):
super(RequestsClient, self).__init__(
verify_ssl_certs=verify_ssl_certs,
proxy=proxy,
async_fallback_client=async_fallback_client,
)
self._session = session
self._timeout = timeout
if _lib is None:
import requests
_lib = requests
self.requests = _lib
def request(
self,
method: str,
url: str,
headers: Optional[Mapping[str, str]],
post_data=None,
) -> Tuple[bytes, int, Mapping[str, str]]:
return self._request_internal(
method, url, headers, post_data, is_streaming=False
)
def request_stream(
self,
method: str,
url: str,
headers: Optional[Mapping[str, str]],
post_data=None,
) -> Tuple[Any, int, Mapping[str, str]]:
return self._request_internal(
method, url, headers, post_data, is_streaming=True
)
@overload
def _request_internal(
self,
method: str,
url: str,
headers: Optional[Mapping[str, str]],
post_data,
is_streaming: Literal[True],
) -> Tuple[Any, int, Mapping[str, str]]: ...
@overload
def _request_internal(
self,
method: str,
url: str,
headers: Optional[Mapping[str, str]],
post_data,
is_streaming: Literal[False],
) -> Tuple[bytes, int, Mapping[str, str]]: ...
def _request_internal(
self,
method: str,
url: str,
headers: Optional[Mapping[str, str]],
post_data,
is_streaming: bool,
) -> Tuple[Union[bytes, Any], int, Mapping[str, str]]:
kwargs = {}
if self._verify_ssl_certs:
kwargs["verify"] = stripe.ca_bundle_path
else:
kwargs["verify"] = False
if self._proxy:
kwargs["proxies"] = self._proxy
if is_streaming:
kwargs["stream"] = True
if getattr(self._thread_local, "session", None) is None:
self._thread_local.session = (
self._session or self.requests.Session()
)
try:
try:
result = cast(
"RequestsSession", self._thread_local.session
).request(
method,
url,
headers=headers,
data=post_data,
timeout=self._timeout,
**kwargs,
)
except TypeError as e:
raise TypeError(
"Warning: It looks like your installed version of the "
'"requests" library is not compatible with Stripe\'s '
"usage thereof. (HINT: The most likely cause is that "
'your "requests" library is out of date. You can fix '
'that by running "pip install -U requests".) The '
"underlying error was: %s" % (e,)
)
if is_streaming:
content = result.raw
else:
# This causes the content to actually be read, which could cause
# e.g. a socket timeout. TODO: The other fetch methods probably
# are susceptible to the same and should be updated.
content = result.content
status_code = result.status_code
except Exception as e:
# Would catch just requests.exceptions.RequestException, but can
# also raise ValueError, RuntimeError, etc.
self._handle_request_error(e)
return content, status_code, result.headers
def _handle_request_error(self, e: Exception) -> NoReturn:
# Catch SSL error first as it belongs to ConnectionError,
# but we don't want to retry
if isinstance(e, self.requests.exceptions.SSLError):
msg = (
"Could not verify Stripe's SSL certificate. Please make "
"sure that your network is not intercepting certificates. "
"If this problem persists, let us know at "
"support@stripe.com."
)
err = "%s: %s" % (type(e).__name__, str(e))
should_retry = False
# Retry only timeout and connect errors; similar to urllib3 Retry
elif isinstance(
e,
(
self.requests.exceptions.Timeout,
self.requests.exceptions.ConnectionError,
),
):
msg = (
"Unexpected error communicating with Stripe. "
"If this problem persists, let us know at "
"support@stripe.com."
)
err = "%s: %s" % (type(e).__name__, str(e))
should_retry = True
# Catch remaining request exceptions
elif isinstance(e, self.requests.exceptions.RequestException):
msg = (
"Unexpected error communicating with Stripe. "
"If this problem persists, let us know at "
"support@stripe.com."
)
err = "%s: %s" % (type(e).__name__, str(e))
should_retry = False
else:
msg = (
"Unexpected error communicating with Stripe. "
"It looks like there's probably a configuration "
"issue locally. If this problem persists, let us "
"know at support@stripe.com."
)
err = "A %s was raised" % (type(e).__name__,)
if str(e):
err += " with error message %s" % (str(e),)
else:
err += " with no error message"
should_retry = False
msg = textwrap.fill(msg) + "\n\n(Network error: %s)" % (err,)
raise APIConnectionError(msg, should_retry=should_retry) from e
def close(self):
if getattr(self._thread_local, "session", None) is not None:
self._thread_local.session.close()
class UrlFetchClient(HTTPClient):
name = "urlfetch"
def __init__(
self,
verify_ssl_certs: bool = True,
proxy: Optional[HTTPClient._Proxy] = None,
deadline: int = 55,
async_fallback_client: Optional[HTTPClient] = None,
_lib=None, # used for internal unit testing
):
super(UrlFetchClient, self).__init__(
verify_ssl_certs=verify_ssl_certs,
proxy=proxy,
async_fallback_client=async_fallback_client,
)
# no proxy support in urlfetch. for a patch, see:
# https://code.google.com/p/googleappengine/issues/detail?id=544
if proxy:
raise ValueError(
"No proxy support in urlfetch library. "
"Set stripe.default_http_client to either RequestsClient, "
"PycurlClient, or UrllibClient instance to use a proxy."
)
self._verify_ssl_certs = verify_ssl_certs
# GAE requests time out after 60 seconds, so make sure to default
# to 55 seconds to allow for a slow Stripe
self._deadline = deadline
if _lib is None:
from google.appengine.api import urlfetch # pyright: ignore
_lib = urlfetch
self.urlfetch = _lib
def request(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[str, int, Mapping[str, str]]:
return self._request_internal(
method, url, headers, post_data, is_streaming=False
)
def request_stream(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[BytesIO, int, Mapping[str, str]]:
return self._request_internal(
method, url, headers, post_data, is_streaming=True
)
@overload
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: Literal[True],
) -> Tuple[BytesIO, int, Any]: ...
@overload
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: Literal[False],
) -> Tuple[str, int, Any]: ...
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming,
):
try:
result = self.urlfetch.fetch(
url=url,
method=method,
headers=headers,
# Google App Engine doesn't let us specify our own cert bundle.
# However, that's ok because the CA bundle they use recognizes
# api.stripe.com.
validate_certificate=self._verify_ssl_certs,
deadline=self._deadline,
payload=post_data,
)
except self.urlfetch.Error as e:
self._handle_request_error(e, url)
if is_streaming:
# This doesn't really stream.
content = BytesIO(str.encode(result.content))
else:
content = result.content
return content, result.status_code, result.headers
def _handle_request_error(self, e: Exception, url: str) -> NoReturn:
if isinstance(e, self.urlfetch.InvalidURLError):
msg = (
"The Stripe library attempted to fetch an "
"invalid URL (%r). This is likely due to a bug "
"in the Stripe Python bindings. Please let us know "
"at support@stripe.com." % (url,)
)
elif isinstance(e, self.urlfetch.DownloadError):
msg = "There was a problem retrieving data from Stripe."
elif isinstance(e, self.urlfetch.ResponseTooLargeError):
msg = (
"There was a problem receiving all of your data from "
"Stripe. This is likely due to a bug in Stripe. "
"Please let us know at support@stripe.com."
)
else:
msg = (
"Unexpected error communicating with Stripe. If this "
"problem persists, let us know at support@stripe.com."
)
msg = textwrap.fill(msg) + "\n\n(Network error: " + str(e) + ")"
raise APIConnectionError(msg) from e
def close(self):
pass
class _Proxy(TypedDict):
http: Optional["ParseResult"]
https: Optional["ParseResult"]
class PycurlClient(HTTPClient):
class _ParsedProxy(TypedDict, total=False):
http: Optional["ParseResult"]
https: Optional["ParseResult"]
name = "pycurl"
_parsed_proxy: Optional[_ParsedProxy]
def __init__(
self,
verify_ssl_certs: bool = True,
proxy: Optional[HTTPClient._Proxy] = None,
async_fallback_client: Optional[HTTPClient] = None,
_lib=None, # used for internal unit testing
):
super(PycurlClient, self).__init__(
verify_ssl_certs=verify_ssl_certs,
proxy=proxy,
async_fallback_client=async_fallback_client,
)
if _lib is None:
import pycurl # pyright: ignore[reportMissingModuleSource]
_lib = pycurl
self.pycurl = _lib
# Initialize this within the object so that we can reuse connections.
self._curl = _lib.Curl()
self._parsed_proxy = {}
# need to urlparse the proxy, since PyCurl
# consumes the proxy url in small pieces
if self._proxy:
from urllib.parse import urlparse
proxy_ = self._proxy
for scheme, value in proxy_.items():
# In general, TypedDict.items() gives you (key: str, value: object)
# but we know value to be a string because all the value types on Proxy_ are strings.
self._parsed_proxy[scheme] = urlparse(cast(str, value))
def parse_headers(self, data):
if "\r\n" not in data:
return {}
raw_headers = data.split("\r\n", 1)[1]
headers = email.message_from_string(raw_headers)
return dict((k.lower(), v) for k, v in dict(headers).items())
def request(
self, method, url, headers: Mapping[str, str], post_data=None
) -> Tuple[str, int, Mapping[str, str]]:
return self._request_internal(
method, url, headers, post_data, is_streaming=False
)
def request_stream(
self, method, url, headers: Mapping[str, str], post_data=None
) -> Tuple[BytesIO, int, Mapping[str, str]]:
return self._request_internal(
method, url, headers, post_data, is_streaming=True
)
@overload
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: Literal[True],
) -> Tuple[BytesIO, int, Any]: ...
@overload
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: Literal[False],
) -> Tuple[str, int, Mapping[str, str]]: ...
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming,
) -> Tuple[Union[str, BytesIO], int, Mapping[str, str]]:
b = BytesIO()
rheaders = BytesIO()
# Pycurl's design is a little weird: although we set per-request
# options on this object, it's also capable of maintaining established
# connections. Here we call reset() between uses to make sure it's in a
# pristine state, but notably reset() doesn't reset connections, so we
# still get to take advantage of those by virtue of re-using the same
# object.
self._curl.reset()
proxy = self._get_proxy(url)
if proxy:
if proxy.hostname:
self._curl.setopt(self.pycurl.PROXY, proxy.hostname)
if proxy.port:
self._curl.setopt(self.pycurl.PROXYPORT, proxy.port)
if proxy.username or proxy.password:
self._curl.setopt(
self.pycurl.PROXYUSERPWD,
"%s:%s" % (proxy.username, proxy.password),
)
if method == "get":
self._curl.setopt(self.pycurl.HTTPGET, 1)
elif method == "post":
self._curl.setopt(self.pycurl.POST, 1)
self._curl.setopt(self.pycurl.POSTFIELDS, post_data)
else:
self._curl.setopt(self.pycurl.CUSTOMREQUEST, method.upper())
# pycurl doesn't like unicode URLs
self._curl.setopt(self.pycurl.URL, url)
self._curl.setopt(self.pycurl.WRITEFUNCTION, b.write)
self._curl.setopt(self.pycurl.HEADERFUNCTION, rheaders.write)
self._curl.setopt(self.pycurl.NOSIGNAL, 1)
self._curl.setopt(self.pycurl.CONNECTTIMEOUT, 30)
self._curl.setopt(self.pycurl.TIMEOUT, 80)
self._curl.setopt(
self.pycurl.HTTPHEADER,
["%s: %s" % (k, v) for k, v in dict(headers).items()],
)
if self._verify_ssl_certs:
self._curl.setopt(self.pycurl.CAINFO, stripe.ca_bundle_path)
else:
self._curl.setopt(self.pycurl.SSL_VERIFYHOST, False)
try:
self._curl.perform()
except self.pycurl.error as e:
self._handle_request_error(e)
if is_streaming:
b.seek(0)
rcontent = b
else:
rcontent = b.getvalue().decode("utf-8")
rcode = self._curl.getinfo(self.pycurl.RESPONSE_CODE)
headers = self.parse_headers(rheaders.getvalue().decode("utf-8"))
return rcontent, rcode, headers
def _handle_request_error(self, e: Exception) -> NoReturn:
if e.args[0] in [
self.pycurl.E_COULDNT_CONNECT,
self.pycurl.E_COULDNT_RESOLVE_HOST,
self.pycurl.E_OPERATION_TIMEOUTED,
]:
msg = (
"Could not connect to Stripe. Please check your "
"internet connection and try again. If this problem "
"persists, you should check Stripe's service status at "
"https://twitter.com/stripestatus, or let us know at "
"support@stripe.com."
)
should_retry = True
elif e.args[0] in [
self.pycurl.E_SSL_CACERT,
self.pycurl.E_SSL_PEER_CERTIFICATE,
]:
msg = (
"Could not verify Stripe's SSL certificate. Please make "
"sure that your network is not intercepting certificates. "
"If this problem persists, let us know at "
"support@stripe.com."
)
should_retry = False
else:
msg = (
"Unexpected error communicating with Stripe. If this "
"problem persists, let us know at support@stripe.com."
)
should_retry = False
msg = textwrap.fill(msg) + "\n\n(Network error: " + e.args[1] + ")"
raise APIConnectionError(msg, should_retry=should_retry) from e
def _get_proxy(self, url) -> Optional["ParseResult"]:
if self._parsed_proxy:
proxy = self._parsed_proxy
scheme = url.split(":")[0] if url else None
if scheme:
return proxy.get(scheme, proxy.get(scheme[0:-1]))
return None
def close(self):
pass
class UrllibClient(HTTPClient):
name = "urllib.request"
def __init__(
self,
verify_ssl_certs: bool = True,
proxy: Optional[HTTPClient._Proxy] = None,
async_fallback_client: Optional[HTTPClient] = None,
_lib=None, # used for internal unit testing
):
super(UrllibClient, self).__init__(
verify_ssl_certs=verify_ssl_certs,
proxy=proxy,
async_fallback_client=async_fallback_client,
)
if _lib is None:
import urllib.request as urllibrequest
_lib = urllibrequest
self.urllibrequest = _lib
import urllib.error as urlliberror
self.urlliberror = urlliberror
# prepare and cache proxy tied opener here
self._opener = None
if self._proxy:
# We have to cast _Proxy to Dict[str, str] because pyright is not smart enough to
# realize that all the value types are str.
proxy_handler = self.urllibrequest.ProxyHandler(
cast(Dict[str, str], self._proxy)
)
self._opener = self.urllibrequest.build_opener(proxy_handler)
def request(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[str, int, Mapping[str, str]]:
return self._request_internal(
method, url, headers, post_data, is_streaming=False
)
def request_stream(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[HTTPResponse, int, Mapping[str, str]]:
return self._request_internal(
method, url, headers, post_data, is_streaming=True
)
@overload
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: Literal[False],
) -> Tuple[str, int, Any]: ...
@overload
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming: Literal[True],
) -> Tuple[HTTPResponse, int, Any]: ...
def _request_internal(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data,
is_streaming,
):
if isinstance(post_data, str):
post_data = post_data.encode("utf-8")
req = self.urllibrequest.Request(
url, post_data, cast(MutableMapping[str, str], headers)
)
if method not in ("get", "post"):
req.get_method = lambda: method.upper()
try:
# use the custom proxy tied opener, if any.
# otherwise, fall to the default urllib opener.
response = (
self._opener.open(req)
if self._opener
else self.urllibrequest.urlopen(req)
)
if is_streaming:
rcontent = response
else:
rcontent = response.read()
rcode = response.code
headers = dict(response.info())
except self.urlliberror.HTTPError as e:
rcode = e.code
rcontent = e.read()
headers = dict(e.info())
except (self.urlliberror.URLError, ValueError) as e:
self._handle_request_error(e)
lh = dict((k.lower(), v) for k, v in iter(dict(headers).items()))
return rcontent, rcode, lh
def _handle_request_error(self, e: Exception) -> NoReturn:
msg = (
"Unexpected error communicating with Stripe. "
"If this problem persists, let us know at support@stripe.com."
)
msg = textwrap.fill(msg) + "\n\n(Network error: " + str(e) + ")"
raise APIConnectionError(msg) from e
def close(self):
pass
class HTTPXClient(HTTPClient):
name = "httpx"
_client: Optional["HTTPXClientType"]
def __init__(
self,
timeout: Optional[Union[float, "HTTPXTimeout"]] = 80,
allow_sync_methods=False,
_lib=None, # used for internal unit testing
**kwargs,
):
super(HTTPXClient, self).__init__(**kwargs)
if _lib is None:
import httpx
_lib = httpx
self.httpx = _lib
import anyio
self.anyio = anyio
kwargs = {}
if self._verify_ssl_certs:
kwargs["verify"] = ssl.create_default_context(
cafile=stripe.ca_bundle_path
)
else:
kwargs["verify"] = False
self._client_async = self.httpx.AsyncClient(**kwargs)
self._client = None
if allow_sync_methods:
self._client = self.httpx.Client(**kwargs)
self._timeout = timeout
def sleep_async(self, secs):
return self.anyio.sleep(secs)
def _get_request_args_kwargs(
self, method: str, url: str, headers: Mapping[str, str], post_data
):
kwargs = {}
if self._proxy:
kwargs["proxies"] = self._proxy
if self._timeout:
kwargs["timeout"] = self._timeout
return [
(method, url),
{"headers": headers, "data": post_data or {}, **kwargs},
]
def request(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data=None,
) -> Tuple[bytes, int, Mapping[str, str]]:
if self._client is None:
raise RuntimeError(
"Stripe: HTTPXClient was initialized with allow_sync_methods=False, "
"so it cannot be used for synchronous requests."
)
args, kwargs = self._get_request_args_kwargs(
method, url, headers, post_data
)
try:
response = self._client.request(*args, **kwargs)
except Exception as e:
self._handle_request_error(e)
content = response.content
status_code = response.status_code
response_headers = response.headers
return content, status_code, response_headers
async def request_async(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data=None,
) -> Tuple[bytes, int, Mapping[str, str]]:
args, kwargs = self._get_request_args_kwargs(
method, url, headers, post_data
)
try:
response = await self._client_async.request(*args, **kwargs)
except Exception as e:
self._handle_request_error(e)
content = response.content
status_code = response.status_code
response_headers = response.headers
return content, status_code, response_headers
def _handle_request_error(self, e: Exception) -> NoReturn:
msg = (
"Unexpected error communicating with Stripe. If this "
"problem persists, let us know at support@stripe.com."
)
err = "A %s was raised" % (type(e).__name__,)
should_retry = True
msg = textwrap.fill(msg) + "\n\n(Network error: %s)" % (err,)
raise APIConnectionError(msg, should_retry=should_retry) from e
def request_stream(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[Iterable[bytes], int, Mapping[str, str]]:
if self._client is None:
raise RuntimeError(
"Stripe: HTTPXClient was not initialized with allow_sync_methods=True, "
"so it cannot be used for synchronous requests."
)
args, kwargs = self._get_request_args_kwargs(
method, url, headers, post_data
)
try:
response = self._client.send(
request=self._client_async.build_request(*args, **kwargs),
stream=True,
)
except Exception as e:
self._handle_request_error(e)
content = response.iter_bytes()
status_code = response.status_code
headers = response.headers
return content, status_code, headers
async def request_stream_async(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[AsyncIterable[bytes], int, Mapping[str, str]]:
args, kwargs = self._get_request_args_kwargs(
method, url, headers, post_data
)
try:
response = await self._client_async.send(
request=self._client_async.build_request(*args, **kwargs),
stream=True,
)
except Exception as e:
self._handle_request_error(e)
content = response.aiter_bytes()
status_code = response.status_code
headers = response.headers
return content, status_code, headers
def close(self):
if self._client is not None:
self._client.close()
async def close_async(self):
await self._client_async.aclose()
class AIOHTTPClient(HTTPClient):
name = "aiohttp"
def __init__(
self,
timeout: Optional[Union[float, "AIOHTTPTimeout"]] = 80,
_lib=None, # used for internal unit testing
**kwargs,
):
super(AIOHTTPClient, self).__init__(**kwargs)
if _lib is None:
import aiohttp
_lib = aiohttp
self.aiohttp = _lib
self._timeout = timeout
self._cached_session = None
@property
def _session(self):
if self._cached_session is None:
kwargs = {}
if self._verify_ssl_certs:
ssl_context = ssl.create_default_context(
cafile=stripe.ca_bundle_path
)
kwargs["connector"] = self.aiohttp.TCPConnector(
ssl=ssl_context
)
else:
kwargs["connector"] = self.aiohttp.TCPConnector(
verify_ssl=False
)
self._cached_session = self.aiohttp.ClientSession(**kwargs)
return self._cached_session
def sleep_async(self, secs):
return asyncio.sleep(secs)
def request(self) -> Tuple[bytes, int, Mapping[str, str]]:
raise NotImplementedError(
"AIOHTTPClient does not support synchronous requests."
)
def _get_request_args_kwargs(
self, method: str, url: str, headers: Mapping[str, str], post_data
):
args = (method, url)
kwargs = {}
if self._proxy:
if self._proxy["http"] != self._proxy["https"]:
raise ValueError(
"AIOHTTPClient does not support different proxies for HTTP and HTTPS."
)
kwargs["proxy"] = self._proxy["https"]
if self._timeout:
kwargs["timeout"] = self._timeout
kwargs["headers"] = headers
kwargs["data"] = post_data
return args, kwargs
async def request_async(
self,
method: str,
url: str,
headers: Mapping[str, str],
post_data=None,
) -> Tuple[bytes, int, Mapping[str, str]]:
(
content,
status_code,
response_headers,
) = await self.request_stream_async(
method, url, headers, post_data=post_data
)
return (await content.read()), status_code, response_headers
def _handle_request_error(self, e: Exception) -> NoReturn:
msg = (
"Unexpected error communicating with Stripe. If this "
"problem persists, let us know at support@stripe.com."
)
err = "A %s was raised" % (type(e).__name__,)
should_retry = True
msg = textwrap.fill(msg) + "\n\n(Network error: %s)" % (err,)
raise APIConnectionError(msg, should_retry=should_retry) from e
def request_stream(self) -> Tuple[Iterable[bytes], int, Mapping[str, str]]:
raise NotImplementedError(
"AIOHTTPClient does not support synchronous requests."
)
async def request_stream_async(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple["AIOHTTPStreamReader", int, Mapping[str, str]]:
args, kwargs = self._get_request_args_kwargs(
method, url, headers, post_data
)
try:
response = await self._session.request(*args, **kwargs)
except Exception as e:
self._handle_request_error(e)
content = response.content
status_code = response.status
response_headers = response.headers
return content, status_code, response_headers
def close(self):
pass
async def close_async(self):
await self._session.close()
class NoImportFoundAsyncClient(HTTPClient):
def __init__(self, **kwargs):
super(NoImportFoundAsyncClient, self).__init__(**kwargs)
@staticmethod
def raise_async_client_import_error() -> Never:
raise ImportError(
(
"Import httpx not found. To make async http requests,"
"You must either install httpx or define your own"
"async http client by subclassing stripe.HTTPClient"
"and setting stripe.default_http_client to an instance of it."
)
)
async def request_async(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
) -> Tuple[bytes, int, Mapping[str, str]]:
self.raise_async_client_import_error()
async def request_stream_async(
self, method: str, url: str, headers: Mapping[str, str], post_data=None
):
self.raise_async_client_import_error()
async def close_async(self):
self.raise_async_client_import_error()