appengine.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. # SPDX-License-Identifier: MIT
  2. """
  3. This module provides a pool manager that uses Google App Engine's
  4. `URLFetch Service <https://cloud.google.com/appengine/docs/python/urlfetch>`_.
  5. Example usage::
  6. from urllib3 import PoolManager
  7. from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox
  8. if is_appengine_sandbox():
  9. # AppEngineManager uses AppEngine's URLFetch API behind the scenes
  10. http = AppEngineManager()
  11. else:
  12. # PoolManager uses a socket-level API behind the scenes
  13. http = PoolManager()
  14. r = http.request('GET', 'https://google.com/')
  15. There are `limitations <https://cloud.google.com/appengine/docs/python/\
  16. urlfetch/#Python_Quotas_and_limits>`_ to the URLFetch service and it may not be
  17. the best choice for your application. There are three options for using
  18. urllib3 on Google App Engine:
  19. 1. You can use :class:`AppEngineManager` with URLFetch. URLFetch is
  20. cost-effective in many circumstances as long as your usage is within the
  21. limitations.
  22. 2. You can use a normal :class:`~urllib3.PoolManager` by enabling sockets.
  23. Sockets also have `limitations and restrictions
  24. <https://cloud.google.com/appengine/docs/python/sockets/\
  25. #limitations-and-restrictions>`_ and have a lower free quota than URLFetch.
  26. To use sockets, be sure to specify the following in your ``app.yaml``::
  27. env_variables:
  28. GAE_USE_SOCKETS_HTTPLIB : 'true'
  29. 3. If you are using `App Engine Flexible
  30. <https://cloud.google.com/appengine/docs/flexible/>`_, you can use the standard
  31. :class:`PoolManager` without any configuration or special environment variables.
  32. """
  33. from __future__ import absolute_import
  34. import logging
  35. import os
  36. import warnings
  37. from ..packages.six.moves.urllib.parse import urljoin
  38. from ..exceptions import (
  39. HTTPError,
  40. HTTPWarning,
  41. MaxRetryError,
  42. ProtocolError,
  43. TimeoutError,
  44. SSLError
  45. )
  46. from ..packages.six import BytesIO
  47. from ..request import RequestMethods
  48. from ..response import HTTPResponse
  49. from ..util.timeout import Timeout
  50. from ..util.retry import Retry
  51. try:
  52. from google.appengine.api import urlfetch
  53. except ImportError:
  54. urlfetch = None
  55. log = logging.getLogger(__name__)
  56. class AppEnginePlatformWarning(HTTPWarning):
  57. pass
  58. class AppEnginePlatformError(HTTPError):
  59. pass
  60. class AppEngineManager(RequestMethods):
  61. """
  62. Connection manager for Google App Engine sandbox applications.
  63. This manager uses the URLFetch service directly instead of using the
  64. emulated httplib, and is subject to URLFetch limitations as described in
  65. the App Engine documentation `here
  66. <https://cloud.google.com/appengine/docs/python/urlfetch>`_.
  67. Notably it will raise an :class:`AppEnginePlatformError` if:
  68. * URLFetch is not available.
  69. * If you attempt to use this on App Engine Flexible, as full socket
  70. support is available.
  71. * If a request size is more than 10 megabytes.
  72. * If a response size is more than 32 megabtyes.
  73. * If you use an unsupported request method such as OPTIONS.
  74. Beyond those cases, it will raise normal urllib3 errors.
  75. """
  76. def __init__(self, headers=None, retries=None, validate_certificate=True,
  77. urlfetch_retries=True):
  78. if not urlfetch:
  79. raise AppEnginePlatformError(
  80. "URLFetch is not available in this environment.")
  81. if is_prod_appengine_mvms():
  82. raise AppEnginePlatformError(
  83. "Use normal urllib3.PoolManager instead of AppEngineManager"
  84. "on Managed VMs, as using URLFetch is not necessary in "
  85. "this environment.")
  86. warnings.warn(
  87. "urllib3 is using URLFetch on Google App Engine sandbox instead "
  88. "of sockets. To use sockets directly instead of URLFetch see "
  89. "https://urllib3.readthedocs.io/en/latest/reference/urllib3.contrib.html.",
  90. AppEnginePlatformWarning)
  91. RequestMethods.__init__(self, headers)
  92. self.validate_certificate = validate_certificate
  93. self.urlfetch_retries = urlfetch_retries
  94. self.retries = retries or Retry.DEFAULT
  95. def __enter__(self):
  96. return self
  97. def __exit__(self, exc_type, exc_val, exc_tb):
  98. # Return False to re-raise any potential exceptions
  99. return False
  100. def urlopen(self, method, url, body=None, headers=None,
  101. retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT,
  102. **response_kw):
  103. retries = self._get_retries(retries, redirect)
  104. try:
  105. follow_redirects = (
  106. redirect and
  107. retries.redirect != 0 and
  108. retries.total)
  109. response = urlfetch.fetch(
  110. url,
  111. payload=body,
  112. method=method,
  113. headers=headers or {},
  114. allow_truncated=False,
  115. follow_redirects=self.urlfetch_retries and follow_redirects,
  116. deadline=self._get_absolute_timeout(timeout),
  117. validate_certificate=self.validate_certificate,
  118. )
  119. except urlfetch.DeadlineExceededError as e:
  120. raise TimeoutError(self, e)
  121. except urlfetch.InvalidURLError as e:
  122. if 'too large' in str(e):
  123. raise AppEnginePlatformError(
  124. "URLFetch request too large, URLFetch only "
  125. "supports requests up to 10mb in size.", e)
  126. raise ProtocolError(e)
  127. except urlfetch.DownloadError as e:
  128. if 'Too many redirects' in str(e):
  129. raise MaxRetryError(self, url, reason=e)
  130. raise ProtocolError(e)
  131. except urlfetch.ResponseTooLargeError as e:
  132. raise AppEnginePlatformError(
  133. "URLFetch response too large, URLFetch only supports"
  134. "responses up to 32mb in size.", e)
  135. except urlfetch.SSLCertificateError as e:
  136. raise SSLError(e)
  137. except urlfetch.InvalidMethodError as e:
  138. raise AppEnginePlatformError(
  139. "URLFetch does not support method: %s" % method, e)
  140. http_response = self._urlfetch_response_to_http_response(
  141. response, retries=retries, **response_kw)
  142. # Handle redirect?
  143. redirect_location = redirect and http_response.get_redirect_location()
  144. if redirect_location:
  145. # Check for redirect response
  146. if (self.urlfetch_retries and retries.raise_on_redirect):
  147. raise MaxRetryError(self, url, "too many redirects")
  148. else:
  149. if http_response.status == 303:
  150. method = 'GET'
  151. try:
  152. retries = retries.increment(method, url, response=http_response, _pool=self)
  153. except MaxRetryError:
  154. if retries.raise_on_redirect:
  155. raise MaxRetryError(self, url, "too many redirects")
  156. return http_response
  157. retries.sleep_for_retry(http_response)
  158. log.debug("Redirecting %s -> %s", url, redirect_location)
  159. redirect_url = urljoin(url, redirect_location)
  160. return self.urlopen(
  161. method, redirect_url, body, headers,
  162. retries=retries, redirect=redirect,
  163. timeout=timeout, **response_kw)
  164. # Check if we should retry the HTTP response.
  165. has_retry_after = bool(http_response.getheader('Retry-After'))
  166. if retries.is_retry(method, http_response.status, has_retry_after):
  167. retries = retries.increment(
  168. method, url, response=http_response, _pool=self)
  169. log.debug("Retry: %s", url)
  170. retries.sleep(http_response)
  171. return self.urlopen(
  172. method, url,
  173. body=body, headers=headers,
  174. retries=retries, redirect=redirect,
  175. timeout=timeout, **response_kw)
  176. return http_response
  177. def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw):
  178. if is_prod_appengine():
  179. # Production GAE handles deflate encoding automatically, but does
  180. # not remove the encoding header.
  181. content_encoding = urlfetch_resp.headers.get('content-encoding')
  182. if content_encoding == 'deflate':
  183. del urlfetch_resp.headers['content-encoding']
  184. transfer_encoding = urlfetch_resp.headers.get('transfer-encoding')
  185. # We have a full response's content,
  186. # so let's make sure we don't report ourselves as chunked data.
  187. if transfer_encoding == 'chunked':
  188. encodings = transfer_encoding.split(",")
  189. encodings.remove('chunked')
  190. urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings)
  191. return HTTPResponse(
  192. # In order for decoding to work, we must present the content as
  193. # a file-like object.
  194. body=BytesIO(urlfetch_resp.content),
  195. headers=urlfetch_resp.headers,
  196. status=urlfetch_resp.status_code,
  197. **response_kw
  198. )
  199. def _get_absolute_timeout(self, timeout):
  200. if timeout is Timeout.DEFAULT_TIMEOUT:
  201. return None # Defer to URLFetch's default.
  202. if isinstance(timeout, Timeout):
  203. if timeout._read is not None or timeout._connect is not None:
  204. warnings.warn(
  205. "URLFetch does not support granular timeout settings, "
  206. "reverting to total or default URLFetch timeout.",
  207. AppEnginePlatformWarning)
  208. return timeout.total
  209. return timeout
  210. def _get_retries(self, retries, redirect):
  211. if not isinstance(retries, Retry):
  212. retries = Retry.from_int(
  213. retries, redirect=redirect, default=self.retries)
  214. if retries.connect or retries.read or retries.redirect:
  215. warnings.warn(
  216. "URLFetch only supports total retries and does not "
  217. "recognize connect, read, or redirect retry parameters.",
  218. AppEnginePlatformWarning)
  219. return retries
  220. def is_appengine():
  221. return (is_local_appengine() or
  222. is_prod_appengine() or
  223. is_prod_appengine_mvms())
  224. def is_appengine_sandbox():
  225. return is_appengine() and not is_prod_appengine_mvms()
  226. def is_local_appengine():
  227. return ('APPENGINE_RUNTIME' in os.environ and
  228. 'Development/' in os.environ['SERVER_SOFTWARE'])
  229. def is_prod_appengine():
  230. return ('APPENGINE_RUNTIME' in os.environ and
  231. 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and
  232. not is_prod_appengine_mvms())
  233. def is_prod_appengine_mvms():
  234. return os.environ.get('GAE_VM', False) == 'true'