1 from __future__ import absolute_import
6 from ..exceptions import (
15 from ..packages.six import BytesIO
16 from ..request import RequestMethods
17 from ..response import HTTPResponse
18 from ..util.timeout import Timeout
19 from ..util.retry import Retry
22 from google.appengine.api import urlfetch
27 log = logging.getLogger(__name__)
30 class AppEnginePlatformWarning(HTTPWarning):
34 class AppEnginePlatformError(HTTPError):
38 class AppEngineManager(RequestMethods):
40 Connection manager for Google App Engine sandbox applications.
42 This manager uses the URLFetch service directly instead of using the
43 emulated httplib, and is subject to URLFetch limitations as described in
44 the App Engine documentation here:
46 https://cloud.google.com/appengine/docs/python/urlfetch
48 Notably it will raise an AppEnginePlatformError if:
49 * URLFetch is not available.
50 * If you attempt to use this on GAEv2 (Managed VMs), as full socket
52 * If a request size is more than 10 megabytes.
53 * If a response size is more than 32 megabtyes.
54 * If you use an unsupported request method such as OPTIONS.
56 Beyond those cases, it will raise normal urllib3 errors.
59 def __init__(self, headers=None, retries=None, validate_certificate=True):
61 raise AppEnginePlatformError(
62 "URLFetch is not available in this environment.")
64 if is_prod_appengine_mvms():
65 raise AppEnginePlatformError(
66 "Use normal urllib3.PoolManager instead of AppEngineManager"
67 "on Managed VMs, as using URLFetch is not necessary in "
71 "urllib3 is using URLFetch on Google App Engine sandbox instead "
72 "of sockets. To use sockets directly instead of URLFetch see "
73 "https://urllib3.readthedocs.org/en/latest/contrib.html.",
74 AppEnginePlatformWarning)
76 RequestMethods.__init__(self, headers)
77 self.validate_certificate = validate_certificate
79 self.retries = retries or Retry.DEFAULT
84 def __exit__(self, exc_type, exc_val, exc_tb):
85 # Return False to re-raise any potential exceptions
88 def urlopen(self, method, url, body=None, headers=None,
89 retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT,
92 retries = self._get_retries(retries, redirect)
95 response = urlfetch.fetch(
99 headers=headers or {},
100 allow_truncated=False,
103 retries.redirect != 0 and
105 deadline=self._get_absolute_timeout(timeout),
106 validate_certificate=self.validate_certificate,
108 except urlfetch.DeadlineExceededError as e:
109 raise TimeoutError(self, e)
111 except urlfetch.InvalidURLError as e:
112 if 'too large' in str(e):
113 raise AppEnginePlatformError(
114 "URLFetch request too large, URLFetch only "
115 "supports requests up to 10mb in size.", e)
116 raise ProtocolError(e)
118 except urlfetch.DownloadError as e:
119 if 'Too many redirects' in str(e):
120 raise MaxRetryError(self, url, reason=e)
121 raise ProtocolError(e)
123 except urlfetch.ResponseTooLargeError as e:
124 raise AppEnginePlatformError(
125 "URLFetch response too large, URLFetch only supports"
126 "responses up to 32mb in size.", e)
128 except urlfetch.SSLCertificateError as e:
131 except urlfetch.InvalidMethodError as e:
132 raise AppEnginePlatformError(
133 "URLFetch does not support method: %s" % method, e)
135 http_response = self._urlfetch_response_to_http_response(
136 response, **response_kw)
138 # Check for redirect response
139 if (http_response.get_redirect_location() and
140 retries.raise_on_redirect and redirect):
141 raise MaxRetryError(self, url, "too many redirects")
143 # Check if we should retry the HTTP response.
144 if retries.is_forced_retry(method, status_code=http_response.status):
145 retries = retries.increment(
146 method, url, response=http_response, _pool=self)
147 log.info("Forced retry: %s", url)
151 body=body, headers=headers,
152 retries=retries, redirect=redirect,
153 timeout=timeout, **response_kw)
157 def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw):
159 if is_prod_appengine():
160 # Production GAE handles deflate encoding automatically, but does
161 # not remove the encoding header.
162 content_encoding = urlfetch_resp.headers.get('content-encoding')
164 if content_encoding == 'deflate':
165 del urlfetch_resp.headers['content-encoding']
167 transfer_encoding = urlfetch_resp.headers.get('transfer-encoding')
168 # We have a full response's content,
169 # so let's make sure we don't report ourselves as chunked data.
170 if transfer_encoding == 'chunked':
171 encodings = transfer_encoding.split(",")
172 encodings.remove('chunked')
173 urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings)
176 # In order for decoding to work, we must present the content as
177 # a file-like object.
178 body=BytesIO(urlfetch_resp.content),
179 headers=urlfetch_resp.headers,
180 status=urlfetch_resp.status_code,
184 def _get_absolute_timeout(self, timeout):
185 if timeout is Timeout.DEFAULT_TIMEOUT:
186 return 5 # 5s is the default timeout for URLFetch.
187 if isinstance(timeout, Timeout):
188 if timeout._read is not timeout._connect:
190 "URLFetch does not support granular timeout settings, "
191 "reverting to total timeout.", AppEnginePlatformWarning)
195 def _get_retries(self, retries, redirect):
196 if not isinstance(retries, Retry):
197 retries = Retry.from_int(
198 retries, redirect=redirect, default=self.retries)
200 if retries.connect or retries.read or retries.redirect:
202 "URLFetch only supports total retries and does not "
203 "recognize connect, read, or redirect retry parameters.",
204 AppEnginePlatformWarning)
210 return (is_local_appengine() or
211 is_prod_appengine() or
212 is_prod_appengine_mvms())
215 def is_appengine_sandbox():
216 return is_appengine() and not is_prod_appengine_mvms()
219 def is_local_appengine():
220 return ('APPENGINE_RUNTIME' in os.environ and
221 'Development/' in os.environ['SERVER_SOFTWARE'])
224 def is_prod_appengine():
225 return ('APPENGINE_RUNTIME' in os.environ and
226 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and
227 not is_prod_appengine_mvms())
230 def is_prod_appengine_mvms():
231 return os.environ.get('GAE_VM', False) == 'true'