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()