App Engine Python SDK version 1.8.8
[gae.git] / python / google / appengine / tools / devappserver2 / python / request_handler.py
blob6b2257ac745c922204bc668b5f9c8672d4bb9a33
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """A Python request handler.
19 The module must be imported inside that runtime sandbox so that the logging
20 module is the sandboxed version.
21 """
24 import cStringIO
25 import os
26 import sys
27 import traceback
28 import urllib
29 import urlparse
31 import google
33 from google.appengine.api import api_base_pb
34 from google.appengine.api import apiproxy_stub_map
35 from google.appengine.api import appinfo
36 from google.appengine.api.logservice import log_service_pb
37 from google.appengine.api.logservice import logservice
38 from google.appengine.ext.remote_api import remote_api_stub
39 from google.appengine.runtime import background
40 from google.appengine.runtime import request_environment
41 from google.appengine.runtime import runtime
42 from google.appengine.runtime import shutdown
43 from google.appengine.tools.devappserver2 import http_runtime_constants
44 from google.appengine.tools.devappserver2.python import request_state
47 # Copied from httplib; done so we don't have to import httplib which breaks
48 # our httplib "forwarder" as the environment variable that controls which
49 # implementation we get is not yet set.
51 httplib_responses = {
52 100: 'Continue',
53 101: 'Switching Protocols',
55 200: 'OK',
56 201: 'Created',
57 202: 'Accepted',
58 203: 'Non-Authoritative Information',
59 204: 'No Content',
60 205: 'Reset Content',
61 206: 'Partial Content',
63 300: 'Multiple Choices',
64 301: 'Moved Permanently',
65 302: 'Found',
66 303: 'See Other',
67 304: 'Not Modified',
68 305: 'Use Proxy',
69 306: '(Unused)',
70 307: 'Temporary Redirect',
72 400: 'Bad Request',
73 401: 'Unauthorized',
74 402: 'Payment Required',
75 403: 'Forbidden',
76 404: 'Not Found',
77 405: 'Method Not Allowed',
78 406: 'Not Acceptable',
79 407: 'Proxy Authentication Required',
80 408: 'Request Timeout',
81 409: 'Conflict',
82 410: 'Gone',
83 411: 'Length Required',
84 412: 'Precondition Failed',
85 413: 'Request Entity Too Large',
86 414: 'Request-URI Too Long',
87 415: 'Unsupported Media Type',
88 416: 'Requested Range Not Satisfiable',
89 417: 'Expectation Failed',
91 500: 'Internal Server Error',
92 501: 'Not Implemented',
93 502: 'Bad Gateway',
94 503: 'Service Unavailable',
95 504: 'Gateway Timeout',
96 505: 'HTTP Version Not Supported',
99 class RequestHandler(object):
100 """A WSGI application that forwards requests to a user-provided app."""
102 _PYTHON_LIB_DIR = os.path.dirname(os.path.dirname(google.__file__))
104 def __init__(self, config):
105 self.config = config
106 if appinfo.MODULE_SEPARATOR not in config.version_id:
107 module_id = appinfo.DEFAULT_MODULE
108 version_id = config.version_id
109 else:
110 module_id, version_id = config.version_id.split(appinfo.MODULE_SEPARATOR)
112 self.environ_template = {
113 'APPLICATION_ID': config.app_id,
114 'CURRENT_MODULE_ID': module_id,
115 'CURRENT_VERSION_ID': version_id,
116 'DATACENTER': config.datacenter.encode('ascii'),
117 'INSTANCE_ID': config.instance_id.encode('ascii'),
118 'APPENGINE_RUNTIME': 'python27',
119 'AUTH_DOMAIN': config.auth_domain.encode('ascii'),
120 'HTTPS': 'off',
121 'SCRIPT_NAME': '',
122 'SERVER_SOFTWARE': http_runtime_constants.SERVER_SOFTWARE,
123 'TZ': 'UTC',
124 'wsgi.multithread': config.threadsafe,
126 self._command_globals = {} # Use to evaluate interactive requests.
127 self.environ_template.update((env.key, env.value) for env in config.environ)
129 def __call__(self, environ, start_response):
130 remote_api_stub.RemoteStub._SetRequestId(
131 environ[http_runtime_constants.REQUEST_ID_ENVIRON])
132 request_type = environ.pop(http_runtime_constants.REQUEST_TYPE_HEADER, None)
133 request_state.start_request(
134 environ[http_runtime_constants.REQUEST_ID_ENVIRON])
135 try:
136 if request_type == 'background':
137 response = self.handle_background_request(environ)
138 elif request_type == 'shutdown':
139 response = self.handle_shutdown_request(environ)
140 elif request_type == 'interactive':
141 response = self.handle_interactive_request(environ)
142 else:
143 response = self.handle_normal_request(environ)
144 finally:
145 request_state.end_request(
146 environ[http_runtime_constants.REQUEST_ID_ENVIRON])
147 error = response.get('error', 0)
148 self._flush_logs(response.get('logs', []))
149 if error == 0:
150 response_code = response.get('response_code', 200)
151 status = '%d %s' % (response_code, httplib_responses.get(
152 response_code, 'Unknown Status Code'))
153 start_response(status, response.get('headers', []))
154 return [response.get('body', '')]
155 elif error == 2:
156 start_response('404 Not Found', [])
157 return []
158 else:
159 start_response('500 Internal Server Error',
160 [(http_runtime_constants.ERROR_CODE_HEADER, str(error))])
161 return []
163 def handle_normal_request(self, environ):
164 user_environ = self.get_user_environ(environ)
165 script = environ.pop(http_runtime_constants.SCRIPT_HEADER)
166 body = environ['wsgi.input'].read(int(environ.get('CONTENT_LENGTH', 0)))
167 url = 'http://%s:%s%s?%s' % (user_environ['SERVER_NAME'],
168 user_environ['SERVER_PORT'],
169 urllib.quote(environ['PATH_INFO']),
170 environ['QUERY_STRING'])
171 return runtime.HandleRequest(user_environ, script, url, body,
172 self.config.application_root,
173 self._PYTHON_LIB_DIR)
175 def handle_background_request(self, environ):
176 return background.Handle(self.get_user_environ(environ))
178 def handle_shutdown_request(self, environ):
179 response, exc = shutdown.Handle(self.get_user_environ(environ))
180 if exc:
181 for request in request_state.get_request_states():
182 if (request.request_id !=
183 environ[http_runtime_constants.REQUEST_ID_ENVIRON]):
184 request.inject_exception(exc[1])
185 return response
187 def handle_interactive_request(self, environ):
188 code = environ['wsgi.input'].read().replace('\r\n', '\n')
190 user_environ = self.get_user_environ(environ)
191 if 'HTTP_CONTENT_LENGTH' in user_environ:
192 del user_environ['HTTP_CONTENT_LENGTH']
193 user_environ['REQUEST_METHOD'] = 'GET'
194 url = 'http://%s:%s%s?%s' % (user_environ['SERVER_NAME'],
195 user_environ['SERVER_PORT'],
196 urllib.quote(environ['PATH_INFO']),
197 environ['QUERY_STRING'])
199 results_io = cStringIO.StringIO()
200 old_sys_stdout = sys.stdout
202 try:
203 error = logservice.LogsBuffer()
204 request_environment.current_request.Init(error, user_environ)
205 url = urlparse.urlsplit(url)
206 environ.update(runtime.CgiDictFromParsedUrl(url))
207 sys.stdout = results_io
208 try:
209 try:
210 __import__('appengine_config', self._command_globals)
211 except ImportError as e:
212 if 'appengine_config' not in e.message:
213 raise
214 compiled_code = compile(code, '<string>', 'exec')
215 exec(compiled_code, self._command_globals)
216 except:
217 traceback.print_exc(file=results_io)
219 return {'error': 0,
220 'response_code': 200,
221 'headers': [('Content-Type', 'text/plain')],
222 'body': results_io.getvalue(),
223 'logs': error.parse_logs()}
224 finally:
225 request_environment.current_request.Clear()
226 sys.stdout = old_sys_stdout
228 def get_user_environ(self, environ):
229 """Returns a dict containing the environ to pass to the user's application.
231 Args:
232 environ: A dict containing the request WSGI environ.
234 Returns:
235 A dict containing the environ representing an HTTP request.
237 user_environ = self.environ_template.copy()
238 self.copy_headers(environ, user_environ)
239 user_environ['REQUEST_METHOD'] = environ.get('REQUEST_METHOD', 'GET')
240 content_type = environ.get('CONTENT_TYPE')
241 if content_type:
242 user_environ['HTTP_CONTENT_TYPE'] = content_type
243 content_length = environ.get('CONTENT_LENGTH')
244 if content_length:
245 user_environ['HTTP_CONTENT_LENGTH'] = content_length
246 return user_environ
248 def copy_headers(self, source_environ, dest_environ):
249 """Copy headers from source_environ to dest_environ.
251 This extracts headers that represent environ values and propagates all
252 other headers which are not used for internal implementation details or
253 headers that are stripped.
255 Args:
256 source_environ: The source environ dict.
257 dest_environ: The environ dict to populate.
259 for env in http_runtime_constants.ENVIRONS_TO_PROPAGATE:
260 value = source_environ.get(
261 http_runtime_constants.INTERNAL_ENVIRON_PREFIX + env, None)
262 if value is not None:
263 dest_environ[env] = value
264 for name, value in source_environ.items():
265 if (name.startswith('HTTP_') and
266 not name.startswith(http_runtime_constants.INTERNAL_ENVIRON_PREFIX)):
267 dest_environ[name] = value
269 def _flush_logs(self, logs):
270 """Flushes logs using the LogService API.
272 Args:
273 logs: A list of tuples (timestamp_usec, level, message).
275 logs_group = log_service_pb.UserAppLogGroup()
276 for timestamp_usec, level, message in logs:
277 log_line = logs_group.add_log_line()
278 log_line.set_timestamp_usec(timestamp_usec)
279 log_line.set_level(level)
280 log_line.set_message(message)
281 request = log_service_pb.FlushRequest()
282 request.set_logs(logs_group.Encode())
283 response = api_base_pb.VoidProto()
284 apiproxy_stub_map.MakeSyncCall('logservice', 'Flush', request, response)