App Engine Python SDK version 1.8.2
[gae.git] / python / google / appengine / tools / devappserver2 / endpoints / endpoints_server.py
blob77d9e0fc8dab9e08c92acd4914aa6b303f98b1c3
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 """Helper for Cloud Endpoints API server in the development app server.
19 This is a fake apiserver proxy that does simple transforms on requests that
20 come in to /_ah/api and then re-dispatches them to /_ah/spi. It does not do
21 any authentication, quota checking, DoS checking, etc.
23 In addition, the proxy loads api configs from
24 /_ah/spi/BackendService.getApiConfigs prior to each call, in case the
25 configuration has changed.
26 """
30 import httplib
31 import json
32 import logging
33 import re
34 import wsgiref
36 from google.appengine.tools.devappserver2.endpoints import api_config_manager
37 from google.appengine.tools.devappserver2.endpoints import api_request
38 from google.appengine.tools.devappserver2.endpoints import discovery_api_proxy
39 from google.appengine.tools.devappserver2.endpoints import discovery_service
40 from google.appengine.tools.devappserver2.endpoints import errors
41 from google.appengine.tools.devappserver2.endpoints import util
44 __all__ = ['API_SERVING_PATTERN',
45 'EndpointsDispatcher']
48 # Pattern for paths handled by this module.
49 API_SERVING_PATTERN = '_ah/api/.*'
51 _SPI_ROOT_FORMAT = '/_ah/spi/%s'
52 _SERVER_SOURCE_IP = '0.2.0.3'
54 # Internal constants
55 _CORS_HEADER_ORIGIN = 'Origin'
56 _CORS_HEADER_REQUEST_METHOD = 'Access-Control-Request-Method'
57 _CORS_HEADER_REQUEST_HEADERS = 'Access-Control-Request-Headers'
58 _CORS_HEADER_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
59 _CORS_HEADER_ALLOW_METHODS = 'Access-Control-Allow-Methods'
60 _CORS_HEADER_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
61 _CORS_ALLOWED_METHODS = frozenset(('DELETE', 'GET', 'PATCH', 'POST', 'PUT'))
64 class EndpointsDispatcher(object):
65 """Dispatcher that handles requests to the built-in apiserver handlers."""
67 _API_EXPLORER_URL = 'https://developers.google.com/apis-explorer/?base='
69 def __init__(self, dispatcher, config_manager=None):
70 """Constructor for EndpointsDispatcher.
72 Args:
73 dispatcher: A Dispatcher instance that can be used to make HTTP requests.
74 config_manager: An ApiConfigManager instance that allows a caller to
75 set up an existing configuration for testing.
76 """
77 self._dispatcher = dispatcher
78 if config_manager is None:
79 config_manager = api_config_manager.ApiConfigManager()
80 self.config_manager = config_manager
81 self._dispatchers = []
82 self._add_dispatcher('/_ah/api/explorer/?$',
83 self.handle_api_explorer_request)
84 self._add_dispatcher('/_ah/api/static/.*$',
85 self.handle_api_static_request)
87 def _add_dispatcher(self, path_regex, dispatch_function):
88 """Add a request path and dispatch handler.
90 Args:
91 path_regex: A string regex, the path to match against incoming requests.
92 dispatch_function: The function to call for these requests. The function
93 should take (request, start_response) as arguments and
94 return the contents of the response body.
95 """
96 self._dispatchers.append((re.compile(path_regex), dispatch_function))
98 def __call__(self, environ, start_response):
99 """Handle an incoming request.
101 Args:
102 environ: An environ dict for the request as defined in PEP-333.
103 start_response: A function used to begin the response to the caller.
104 This follows the semantics defined in PEP-333. In particular, it's
105 called with (status, response_headers, exc_info=None), and it returns
106 an object with a write(body_data) function that can be used to write
107 the body of the response.
109 Yields:
110 An iterable over strings containing the body of the HTTP response.
112 request = api_request.ApiRequest(environ)
114 # PEP-333 requires that we return an iterator that iterates over the
115 # response body. Yielding the returned body accomplishes this.
116 yield self.dispatch(request, start_response)
118 def dispatch(self, request, start_response):
119 """Handles dispatch to apiserver handlers.
121 This typically ends up calling start_response and returning the entire
122 body of the response.
124 Args:
125 request: An ApiRequest, the request from the user.
126 start_response: A function with semantics defined in PEP-333.
128 Returns:
129 A string, the body of the response.
131 # Check if this matches any of our special handlers.
132 dispatched_response = self.dispatch_non_api_requests(request,
133 start_response)
134 if dispatched_response is not None:
135 return dispatched_response
137 # Get API configuration first. We need this so we know how to
138 # call the back end.
139 api_config_response = self.get_api_configs()
140 if not self.handle_get_api_configs_response(api_config_response):
141 return self.fail_request(request, 'BackendService.getApiConfigs Error',
142 start_response)
144 # Call the service.
145 try:
146 return self.call_spi(request, start_response)
147 except errors.RequestError as error:
148 return self._handle_request_error(request, error, start_response)
150 def dispatch_non_api_requests(self, request, start_response):
151 """Dispatch this request if this is a request to a reserved URL.
153 If the request matches one of our reserved URLs, this calls
154 start_response and returns the response body.
156 Args:
157 request: An ApiRequest, the request from the user.
158 start_response: A function with semantics defined in PEP-333.
160 Returns:
161 None if the request doesn't match one of the reserved URLs this
162 handles. Otherwise, returns the response body.
164 for path_regex, dispatch_function in self._dispatchers:
165 if path_regex.match(request.relative_url):
166 return dispatch_function(request, start_response)
167 return None
169 def handle_api_explorer_request(self, request, start_response):
170 """Handler for requests to _ah/api/explorer.
172 This calls start_response and returns the response body.
174 Args:
175 request: An ApiRequest, the request from the user.
176 start_response: A function with semantics defined in PEP-333.
178 Returns:
179 A string containing the response body (which is empty, in this case).
181 base_url = 'http://%s:%s/_ah/api' % (request.server, request.port)
182 redirect_url = self._API_EXPLORER_URL + base_url
183 return util.send_wsgi_redirect_response(redirect_url, start_response)
185 def handle_api_static_request(self, request, start_response):
186 """Handler for requests to _ah/api/static/.*.
188 This calls start_response and returns the response body.
190 Args:
191 request: An ApiRequest, the request from the user.
192 start_response: A function with semantics defined in PEP-333.
194 Returns:
195 A string containing the response body.
197 discovery_api = discovery_api_proxy.DiscoveryApiProxy()
198 response, body = discovery_api.get_static_file(request.relative_url)
199 status_string = '%d %s' % (response.status, response.reason)
200 if response.status == 200:
201 # Some of the headers that come back from the server can't be passed
202 # along in our response. Specifically, the response from the server has
203 # transfer-encoding: chunked, which doesn't apply to the response that
204 # we're forwarding. There may be other problematic headers, so we strip
205 # off everything but Content-Type.
206 return util.send_wsgi_response(status_string,
207 [('Content-Type',
208 response.getheader('Content-Type'))],
209 body, start_response)
210 else:
211 logging.error('Discovery API proxy failed on %s with %d. Details: %s',
212 request.relative_url, response.status, body)
213 return util.send_wsgi_response(status_string, response.getheaders(), body,
214 start_response)
216 def get_api_configs(self):
217 """Makes a call to the BackendService.getApiConfigs endpoint.
219 Returns:
220 A ResponseTuple containing the response information from the HTTP
221 request.
223 headers = [('Content-Type', 'application/json')]
224 request_body = '{}'
225 response = self._dispatcher.add_request(
226 'POST', '/_ah/spi/BackendService.getApiConfigs',
227 headers, request_body, _SERVER_SOURCE_IP)
228 return response
230 @staticmethod
231 def verify_response(response, status_code, content_type=None):
232 """Verifies that a response has the expected status and content type.
234 Args:
235 response: The ResponseTuple to be checked.
236 status_code: An int, the HTTP status code to be compared with response
237 status.
238 content_type: A string with the acceptable Content-Type header value.
239 None allows any content type.
241 Returns:
242 True if both status_code and content_type match, else False.
244 status = int(response.status.split(' ', 1)[0])
245 if status != status_code:
246 return False
247 if content_type is None:
248 return True
249 for header, value in response.headers:
250 if header.lower() == 'content-type':
251 return value == content_type
252 else:
253 return False
255 def handle_get_api_configs_response(self, api_config_response):
256 """Parses the result of GetApiConfigs and stores its information.
258 Args:
259 api_config_response: The ResponseTuple from the GetApiConfigs call.
261 Returns:
262 True on success, False on failure
264 if self.verify_response(api_config_response, 200, 'application/json'):
265 self.config_manager.parse_api_config_response(
266 api_config_response.content)
267 return True
268 else:
269 return False
271 def call_spi(self, orig_request, start_response):
272 """Generate SPI call (from earlier-saved request).
274 This calls start_response and returns the response body.
276 Args:
277 orig_request: An ApiRequest, the original request from the user.
278 start_response: A function with semantics defined in PEP-333.
280 Returns:
281 A string containing the response body.
283 if orig_request.is_rpc():
284 method_config = self.lookup_rpc_method(orig_request)
285 params = None
286 else:
287 method_config, params = self.lookup_rest_method(orig_request)
288 if not method_config:
289 cors_handler = EndpointsDispatcher.__CheckCorsHeaders(orig_request)
290 return util.send_wsgi_not_found_response(start_response,
291 cors_handler=cors_handler)
293 # Prepare the request for the back end.
294 spi_request = self.transform_request(orig_request, params, method_config)
296 # Check if this SPI call is for the Discovery service. If so, route
297 # it to our Discovery handler.
298 discovery = discovery_service.DiscoveryService(self.config_manager)
299 discovery_response = discovery.handle_discovery_request(
300 spi_request.path, spi_request, start_response)
301 if discovery_response:
302 return discovery_response
304 # Send the request to the user's SPI handlers.
305 url = _SPI_ROOT_FORMAT % spi_request.path
306 spi_request.headers['Content-Type'] = 'application/json'
307 response = self._dispatcher.add_request('POST', url,
308 spi_request.headers.items(),
309 spi_request.body,
310 spi_request.source_ip)
311 return self.handle_spi_response(orig_request, spi_request, response,
312 start_response)
314 class __CheckCorsHeaders(object):
315 """Track information about CORS headers and our response to them."""
317 def __init__(self, request):
318 self.allow_cors_request = False
319 self.origin = None
320 self.cors_request_method = None
321 self.cors_request_headers = None
323 self.__check_cors_request(request)
325 def __check_cors_request(self, request):
326 """Check for a CORS request, and see if it gets a CORS response."""
327 # Check for incoming CORS headers.
328 self.origin = request.headers[_CORS_HEADER_ORIGIN]
329 self.cors_request_method = request.headers[_CORS_HEADER_REQUEST_METHOD]
330 self.cors_request_headers = request.headers[
331 _CORS_HEADER_REQUEST_HEADERS]
333 # Check if the request should get a CORS response.
334 if (self.origin and
335 ((self.cors_request_method is None) or
336 (self.cors_request_method.upper() in _CORS_ALLOWED_METHODS))):
337 self.allow_cors_request = True
339 def update_headers(self, headers_in):
340 """Add CORS headers to the response, if needed."""
341 if not self.allow_cors_request:
342 return
344 # Add CORS headers.
345 headers = wsgiref.headers.Headers(headers_in)
346 headers[_CORS_HEADER_ALLOW_ORIGIN] = self.origin
347 headers[_CORS_HEADER_ALLOW_METHODS] = ','.join(tuple(
348 _CORS_ALLOWED_METHODS))
349 if self.cors_request_headers is not None:
350 headers[_CORS_HEADER_ALLOW_HEADERS] = self.cors_request_headers
352 def handle_spi_response(self, orig_request, spi_request, response,
353 start_response):
354 """Handle SPI response, transforming output as needed.
356 This calls start_response and returns the response body.
358 Args:
359 orig_request: An ApiRequest, the original request from the user.
360 spi_request: An ApiRequest, the transformed request that was sent to the
361 SPI handler.
362 response: A ResponseTuple, the response from the SPI handler.
363 start_response: A function with semantics defined in PEP-333.
365 Returns:
366 A string containing the response body.
368 # Verify that the response is json. If it isn't treat, the body as an
369 # error message and wrap it in a json error response.
370 for header, value in response.headers:
371 if (header.lower() == 'content-type' and
372 not value.lower().startswith('application/json')):
373 return self.fail_request(orig_request,
374 'Non-JSON reply: %s' % response.content,
375 start_response)
377 self.check_error_response(response)
379 # Need to check is_rpc() against the original request, because the
380 # incoming request here has had its path modified.
381 if orig_request.is_rpc():
382 body = self.transform_jsonrpc_response(spi_request, response.content)
383 else:
384 body = self.transform_rest_response(response.content)
386 cors_handler = EndpointsDispatcher.__CheckCorsHeaders(orig_request)
387 return util.send_wsgi_response(response.status, response.headers, body,
388 start_response, cors_handler=cors_handler)
390 def fail_request(self, orig_request, message, start_response):
391 """Write an immediate failure response to outfile, no redirect.
393 This calls start_response and returns the error body.
395 Args:
396 orig_request: An ApiRequest, the original request from the user.
397 message: A string containing the error message to be displayed to user.
398 start_response: A function with semantics defined in PEP-333.
400 Returns:
401 A string containing the body of the error response.
403 cors_handler = EndpointsDispatcher.__CheckCorsHeaders(orig_request)
404 return util.send_wsgi_error_response(message, start_response,
405 cors_handler=cors_handler)
407 def lookup_rest_method(self, orig_request):
408 """Looks up and returns rest method for the currently-pending request.
410 Args:
411 orig_request: An ApiRequest, the original request from the user.
413 Returns:
414 A tuple of (method descriptor, parameters), or (None, None) if no method
415 was found for the current request.
417 method_name, method, params = self.config_manager.lookup_rest_method(
418 orig_request.path, orig_request.http_method)
419 orig_request.method_name = method_name
420 return method, params
422 def lookup_rpc_method(self, orig_request):
423 """Looks up and returns RPC method for the currently-pending request.
425 Args:
426 orig_request: An ApiRequest, the original request from the user.
428 Returns:
429 The RPC method descriptor that was found for the current request, or None
430 if none was found.
432 if not orig_request.body_json:
433 return None
434 method_name = orig_request.body_json.get('method', '')
435 version = orig_request.body_json.get('apiVersion', '')
436 orig_request.method_name = method_name
437 return self.config_manager.lookup_rpc_method(method_name, version)
439 def transform_request(self, orig_request, params, method_config):
440 """Transforms orig_request to apiserving request.
442 This method uses orig_request to determine the currently-pending request
443 and returns a new transformed request ready to send to the SPI. This
444 method accepts a rest-style or RPC-style request.
446 Args:
447 orig_request: An ApiRequest, the original request from the user.
448 params: A dictionary containing path parameters for rest requests, or
449 None for an RPC request.
450 method_config: A dict, the API config of the method to be called.
452 Returns:
453 An ApiRequest that's a copy of the current request, modified so it can
454 be sent to the SPI. The path is updated and parts of the body or other
455 properties may also be changed.
457 if orig_request.is_rpc():
458 request = self.transform_jsonrpc_request(orig_request)
459 else:
460 method_params = method_config.get('request', {}).get('parameters', {})
461 request = self.transform_rest_request(orig_request, params, method_params)
462 request.path = method_config.get('rosyMethod', '')
463 return request
465 def _check_enum(self, parameter_name, value, field_parameter):
466 """Checks if the parameter value is valid if an enum.
468 If the parameter is not an enum, does nothing. If it is, verifies that
469 its value is valid.
471 Args:
472 parameter_name: A string containing the name of the parameter, which is
473 either just a variable name or the name with the index appended. For
474 example 'var' or 'var[2]'.
475 value: A string or list of strings containing the value(s) to be used as
476 enum(s) for the parameter.
477 field_parameter: The dictionary containing information specific to the
478 field in question. This is retrieved from request.parameters in the
479 method config.
481 Raises:
482 EnumRejectionError: If the given value is not among the accepted
483 enum values in the field parameter.
485 if 'enum' not in field_parameter:
486 return
488 enum_values = [enum['backendValue']
489 for enum in field_parameter['enum'].values()
490 if 'backendValue' in enum]
491 if value not in enum_values:
492 raise errors.EnumRejectionError(parameter_name, value, enum_values)
494 def _check_parameter(self, parameter_name, value, field_parameter):
495 """Checks if the parameter value is valid against all parameter rules.
497 If the value is a list this will recursively call _check_parameter
498 on the values in the list. Otherwise, it checks all parameter rules for the
499 the current value.
501 In the list case, '[index-of-value]' is appended to the parameter name for
502 error reporting purposes.
504 Currently only checks if value adheres to enum rule, but more checks may be
505 added.
507 Args:
508 parameter_name: A string containing the name of the parameter, which is
509 either just a variable name or the name with the index appended, in the
510 recursive case. For example 'var' or 'var[2]'.
511 value: A string or list of strings containing the value(s) to be used for
512 the parameter.
513 field_parameter: The dictionary containing information specific to the
514 field in question. This is retrieved from request.parameters in the
515 method config.
517 if isinstance(value, list):
518 for index, element in enumerate(value):
519 parameter_name_index = '%s[%d]' % (parameter_name, index)
520 self._check_parameter(parameter_name_index, element, field_parameter)
521 return
523 self._check_enum(parameter_name, value, field_parameter)
525 def _add_message_field(self, field_name, value, params):
526 """Converts a . delimitied field name to a message field in parameters.
528 This adds the field to the params dict, broken out so that message
529 parameters appear as sub-dicts within the outer param.
531 For example:
532 {'a.b.c': ['foo']}
533 becomes:
534 {'a': {'b': {'c': ['foo']}}}
536 Args:
537 field_name: A string containing the '.' delimitied name to be converted
538 into a dictionary.
539 value: The value to be set.
540 params: The dictionary holding all the parameters, where the value is
541 eventually set.
543 if '.' not in field_name:
544 params[field_name] = value
545 return
547 root, remaining = field_name.split('.', 1)
548 sub_params = params.setdefault(root, {})
549 self._add_message_field(remaining, value, sub_params)
551 def _update_from_body(self, destination, source):
552 """Updates the dictionary for an API payload with the request body.
554 The values from the body should override those already in the payload, but
555 for nested fields (message objects) the values can be combined
556 recursively.
558 Args:
559 destination: A dictionary containing an API payload parsed from the
560 path and query parameters in a request.
561 source: A dictionary parsed from the body of the request.
563 for key, value in source.iteritems():
564 destination_value = destination.get(key)
565 if isinstance(value, dict) and isinstance(destination_value, dict):
566 self._update_from_body(destination_value, value)
567 else:
568 destination[key] = value
570 def transform_rest_request(self, orig_request, params, method_parameters):
571 """Translates a Rest request into an apiserving request.
573 This makes a copy of orig_request and transforms it to apiserving
574 format (moving request parameters to the body).
576 The request can receive values from the path, query and body and combine
577 them before sending them along to the SPI server. In cases of collision,
578 objects from the body take precedence over those from the query, which in
579 turn take precedence over those from the path.
581 In the case that a repeated value occurs in both the query and the path,
582 those values can be combined, but if that value also occurred in the body,
583 it would override any other values.
585 In the case of nested values from message fields, non-colliding values
586 from subfields can be combined. For example, if '?a.c=10' occurs in the
587 query string and "{'a': {'b': 11}}" occurs in the body, then they will be
588 combined as
591 'a': {
592 'b': 11,
593 'c': 10,
597 before being sent to the SPI server.
599 Args:
600 orig_request: An ApiRequest, the original request from the user.
601 params: A dict with URL path parameters extracted by the config_manager
602 lookup.
603 method_parameters: A dictionary containing the API configuration for the
604 parameters for the request.
606 Returns:
607 A copy of the current request that's been modified so it can be sent
608 to the SPI. The body is updated to include parameters from the
609 URL.
611 request = orig_request.copy()
612 body_json = {}
614 # Handle parameters from the URL path.
615 for key, value in params.iteritems():
616 # Values need to be in a list to interact with query parameter values
617 # and to account for case of repeated parameters
618 body_json[key] = [value]
620 # Add in parameters from the query string.
621 if request.parameters:
622 # For repeated elements, query and path work together
623 for key, value in request.parameters.iteritems():
624 if key in body_json:
625 body_json[key] = value + body_json[key]
626 else:
627 body_json[key] = value
629 # Validate all parameters we've merged so far and convert any '.' delimited
630 # parameters to nested parameters. We don't use iteritems since we may
631 # modify body_json within the loop. For instance, 'a.b' is not a valid key
632 # and would be replaced with 'a'.
633 for key, value in body_json.items():
634 current_parameter = method_parameters.get(key, {})
635 repeated = current_parameter.get('repeated', False)
637 if not repeated:
638 body_json[key] = body_json[key][0]
640 # Order is important here. Parameter names are dot-delimited in
641 # parameters instead of nested in dictionaries as a message field is, so
642 # we need to call _check_parameter on them before calling
643 # _add_message_field.
645 self._check_parameter(key, body_json[key], current_parameter)
646 # Remove the old key and try to convert to nested message value
647 message_value = body_json.pop(key)
648 self._add_message_field(key, message_value, body_json)
650 # Add in values from the body of the request.
651 if request.body_json:
652 self._update_from_body(body_json, request.body_json)
654 request.body_json = body_json
655 request.body = json.dumps(request.body_json)
656 return request
658 def transform_jsonrpc_request(self, orig_request):
659 """Translates a JsonRpc request/response into apiserving request/response.
661 Args:
662 orig_request: An ApiRequest, the original request from the user.
664 Returns:
665 A new request with the request_id updated and params moved to the body.
667 request = orig_request.copy()
668 request.request_id = request.body_json.get('id')
669 request.body_json = request.body_json.get('params', {})
670 request.body = json.dumps(request.body_json)
671 return request
673 def check_error_response(self, response):
674 """Raise an exception if the response from the SPI was an error.
676 Args:
677 response: A ResponseTuple containing the backend response.
679 Raises:
680 BackendError if the response is an error.
682 status_code = int(response.status.split(' ', 1)[0])
683 if status_code >= 300:
684 raise errors.BackendError(response)
686 def transform_rest_response(self, response_body):
687 """Translates an apiserving REST response so it's ready to return.
689 Currently, the only thing that needs to be fixed here is indentation,
690 so it's consistent with what the live app will return.
692 Args:
693 response_body: A string containing the backend response.
695 Returns:
696 A reformatted version of the response JSON.
698 body_json = json.loads(response_body)
699 return json.dumps(body_json, indent=1, sort_keys=True)
701 def transform_jsonrpc_response(self, spi_request, response_body):
702 """Translates an apiserving response to a JsonRpc response.
704 Args:
705 spi_request: An ApiRequest, the transformed request that was sent to the
706 SPI handler.
707 response_body: A string containing the backend response to transform
708 back to JsonRPC.
710 Returns:
711 A string with the updated, JsonRPC-formatted request body.
713 body_json = {'result': json.loads(response_body)}
714 return self._finish_rpc_response(spi_request.request_id,
715 spi_request.is_batch(), body_json)
717 def _finish_rpc_response(self, request_id, is_batch, body_json):
718 """Finish adding information to a JSON RPC response.
720 Args:
721 request_id: None if the request didn't have a request ID. Otherwise, this
722 is a string containing the request ID for the request.
723 is_batch: A boolean indicating whether the request is a batch request.
724 body_json: A dict containing the JSON body of the response.
726 Returns:
727 A string with the updated, JsonRPC-formatted request body.
729 if request_id is not None:
730 body_json['id'] = request_id
731 if is_batch:
732 body_json = [body_json]
733 return json.dumps(body_json, indent=1, sort_keys=True)
735 def _handle_request_error(self, orig_request, error, start_response):
736 """Handle a request error, converting it to a WSGI response.
738 Args:
739 orig_request: An ApiRequest, the original request from the user.
740 error: A RequestError containing information about the error.
741 start_response: A function with semantics defined in PEP-333.
743 Returns:
744 A string containing the response body.
746 headers = [('Content-Type', 'application/json')]
747 if orig_request.is_rpc():
748 # JSON RPC errors are returned with status 200 OK and the
749 # error details in the body.
750 status_code = 200
751 body = self._finish_rpc_response(orig_request.body_json.get('id'),
752 orig_request.is_batch(),
753 error.rpc_error())
754 else:
755 status_code = error.status_code()
756 body = error.rest_error()
758 response_status = '%d %s' % (status_code,
759 httplib.responses.get(status_code,
760 'Unknown Error'))
761 cors_handler = EndpointsDispatcher.__CheckCorsHeaders(orig_request)
762 return util.send_wsgi_response(response_status, headers, body,
763 start_response, cors_handler=cors_handler)