Merge branch 'main' into avm99963-monorail
Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266
GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/third_party/endpoints/endpoints_dispatcher.py b/third_party/endpoints/endpoints_dispatcher.py
new file mode 100644
index 0000000..83e7acb
--- /dev/null
+++ b/third_party/endpoints/endpoints_dispatcher.py
@@ -0,0 +1,718 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Dispatcher middleware for Cloud Endpoints API server.
+
+This middleware does simple transforms on requests that come into the base path
+and then re-dispatches them to the main backend. It does not do any
+authentication, quota checking, DoS checking, etc.
+
+In addition, the middleware loads API configs prior to each call, in case the
+configuration has changed.
+"""
+
+# pylint: disable=g-bad-name
+from __future__ import absolute_import
+
+from six.moves import cStringIO
+from six.moves import http_client
+import json
+import logging
+import re
+import six
+from six.moves import urllib
+import wsgiref
+
+import pkg_resources
+
+from . import api_config_manager
+from . import api_exceptions
+from . import api_request
+from . import discovery_service
+from . import errors
+from . import parameter_converter
+from . import util
+
+_logger = logging.getLogger(__name__)
+
+
+__all__ = ['EndpointsDispatcherMiddleware']
+
+_SERVER_SOURCE_IP = '0.2.0.3'
+
+# Internal constants
+_CORS_HEADER_ORIGIN = 'Origin'
+_CORS_HEADER_REQUEST_METHOD = 'Access-Control-Request-Method'
+_CORS_HEADER_REQUEST_HEADERS = 'Access-Control-Request-Headers'
+_CORS_HEADER_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
+_CORS_HEADER_ALLOW_METHODS = 'Access-Control-Allow-Methods'
+_CORS_HEADER_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
+_CORS_HEADER_ALLOW_CREDS = 'Access-Control-Allow-Credentials'
+_CORS_HEADER_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
+_CORS_ALLOWED_METHODS = frozenset(('DELETE', 'GET', 'PATCH', 'POST', 'PUT'))
+_CORS_EXPOSED_HEADERS = frozenset(
+ ('Content-Encoding', 'Content-Length', 'Date', 'ETag', 'Server')
+)
+
+PROXY_HTML = pkg_resources.resource_string('endpoints', 'proxy.html')
+PROXY_PATH = 'static/proxy.html'
+
+
+class EndpointsDispatcherMiddleware(object):
+ """Dispatcher that handles requests to the built-in apiserver handlers."""
+
+ _API_EXPLORER_URL = 'https://apis-explorer.appspot.com/apis-explorer/?base='
+
+ def __init__(self, backend_wsgi_app, config_manager=None):
+ """Constructor for EndpointsDispatcherMiddleware.
+
+ Args:
+ backend_wsgi_app: A WSGI server that serves the app's endpoints.
+ config_manager: An ApiConfigManager instance that allows a caller to
+ set up an existing configuration for testing.
+ """
+ if config_manager is None:
+ config_manager = api_config_manager.ApiConfigManager()
+ self.config_manager = config_manager
+
+ self._backend = backend_wsgi_app
+ self._dispatchers = []
+ for base_path in self._backend.base_paths:
+ self._add_dispatcher('%sexplorer/?$' % base_path,
+ self.handle_api_explorer_request)
+ self._add_dispatcher('%sstatic/.*$' % base_path,
+ self.handle_api_static_request)
+
+ # Get API configuration so we know how to call the backend.
+ api_config_response = self.get_api_configs()
+ if api_config_response:
+ self.config_manager.process_api_config_response(api_config_response)
+ else:
+ raise api_exceptions.ApiConfigurationError('get_api_configs() returned no configs')
+
+ def _add_dispatcher(self, path_regex, dispatch_function):
+ """Add a request path and dispatch handler.
+
+ Args:
+ path_regex: A string regex, the path to match against incoming requests.
+ dispatch_function: The function to call for these requests. The function
+ should take (request, start_response) as arguments and
+ return the contents of the response body.
+ """
+ self._dispatchers.append((re.compile(path_regex), dispatch_function))
+
+ def _get_explorer_base_url(self, protocol, server, port, base_path):
+ show_port = ((protocol == 'http' and port != 80) or
+ (protocol != 'http' and port != 443))
+ url = ('{0}://{1}:{2}/{3}'.format(
+ protocol, server, port, base_path.lstrip('/\\')) if show_port else
+ '{0}://{1}/{2}'.format(protocol, server, base_path.lstrip('/\\')))
+
+ return url.rstrip('/\\')
+
+ def _get_explorer_redirect_url(self, server, port, base_path):
+ protocol = 'http' if 'localhost' in server else 'https'
+ base_url = self._get_explorer_base_url(protocol, server, port, base_path)
+ return self._API_EXPLORER_URL + base_url
+
+ def __call__(self, environ, start_response):
+ """Handle an incoming request.
+
+ Args:
+ environ: An environ dict for the request as defined in PEP-333.
+ start_response: A function used to begin the response to the caller.
+ This follows the semantics defined in PEP-333. In particular, it's
+ called with (status, response_headers, exc_info=None), and it returns
+ an object with a write(body_data) function that can be used to write
+ the body of the response.
+
+ Yields:
+ An iterable over strings containing the body of the HTTP response.
+ """
+ request = api_request.ApiRequest(environ,
+ base_paths=self._backend.base_paths)
+
+ # PEP-333 requires that we return an iterator that iterates over the
+ # response body. Yielding the returned body accomplishes this.
+ yield self.dispatch(request, start_response)
+
+ def dispatch(self, request, start_response):
+ """Handles dispatch to apiserver handlers.
+
+ This typically ends up calling start_response and returning the entire
+ body of the response.
+
+ Args:
+ request: An ApiRequest, the request from the user.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ A string, the body of the response.
+ """
+ # Check if this matches any of our special handlers.
+ dispatched_response = self.dispatch_non_api_requests(request,
+ start_response)
+ if dispatched_response is not None:
+ return dispatched_response
+
+ # Call the service.
+ try:
+ return self.call_backend(request, start_response)
+ except errors.RequestError as error:
+ return self._handle_request_error(request, error, start_response)
+
+ def dispatch_non_api_requests(self, request, start_response):
+ """Dispatch this request if this is a request to a reserved URL.
+
+ If the request matches one of our reserved URLs, this calls
+ start_response and returns the response body. This also handles OPTIONS
+ CORS requests.
+
+ Args:
+ request: An ApiRequest, the request from the user.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ None if the request doesn't match one of the reserved URLs this
+ handles. Otherwise, returns the response body.
+ """
+ for path_regex, dispatch_function in self._dispatchers:
+ if path_regex.match(request.relative_url):
+ return dispatch_function(request, start_response)
+
+ if request.http_method == 'OPTIONS':
+ cors_handler = self._create_cors_handler(request)
+ if cors_handler.allow_cors_request:
+ # The server returns 200 rather than 204, for some reason.
+ return util.send_wsgi_response('200', [], '', start_response,
+ cors_handler)
+
+ return None
+
+ def handle_api_explorer_request(self, request, start_response):
+ """Handler for requests to {base_path}/explorer.
+
+ This calls start_response and returns the response body.
+
+ Args:
+ request: An ApiRequest, the request from the user.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ A string containing the response body (which is empty, in this case).
+ """
+ redirect_url = self._get_explorer_redirect_url(
+ request.server, request.port, request.base_path)
+ return util.send_wsgi_redirect_response(redirect_url, start_response)
+
+ def handle_api_static_request(self, request, start_response):
+ """Handler for requests to {base_path}/static/.*.
+
+ This calls start_response and returns the response body.
+
+ Args:
+ request: An ApiRequest, the request from the user.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ A string containing the response body.
+ """
+ if request.path == PROXY_PATH:
+ return util.send_wsgi_response('200 OK',
+ [('Content-Type',
+ 'text/html')],
+ PROXY_HTML, start_response)
+ else:
+ _logger.debug('Unknown static url requested: %s',
+ request.relative_url)
+ return util.send_wsgi_response('404 Not Found', [('Content-Type',
+ 'text/plain')], 'Not Found',
+ start_response)
+
+ def get_api_configs(self):
+ return self._backend.get_api_configs()
+
+ @staticmethod
+ def verify_response(response, status_code, content_type=None):
+ """Verifies that a response has the expected status and content type.
+
+ Args:
+ response: The ResponseTuple to be checked.
+ status_code: An int, the HTTP status code to be compared with response
+ status.
+ content_type: A string with the acceptable Content-Type header value.
+ None allows any content type.
+
+ Returns:
+ True if both status_code and content_type match, else False.
+ """
+ status = int(response.status.split(' ', 1)[0])
+ if status != status_code:
+ return False
+
+ if content_type is None:
+ return True
+
+ for header, value in response.headers:
+ if header.lower() == 'content-type':
+ return value == content_type
+
+ # If we fall through to here, the verification has failed, so return False.
+ return False
+
+ def prepare_backend_environ(self, host, method, relative_url, headers, body,
+ source_ip, port):
+ """Build an environ object for the backend to consume.
+
+ Args:
+ host: A string containing the host serving the request.
+ method: A string containing the HTTP method of the request.
+ relative_url: A string containing path and query string of the request.
+ headers: A list of (key, value) tuples where key and value are both
+ strings.
+ body: A string containing the request body.
+ source_ip: The source IP address for the request.
+ port: The port to which to direct the request.
+
+ Returns:
+ An environ object with all the information necessary for the backend to
+ process the request.
+ """
+ body = six.ensure_str(body, 'ascii')
+
+ url = urllib.parse.urlsplit(relative_url)
+ if port != 80:
+ host = '%s:%s' % (host, port)
+ else:
+ host = host
+ environ = {'CONTENT_LENGTH': str(len(body)),
+ 'PATH_INFO': url.path,
+ 'QUERY_STRING': url.query,
+ 'REQUEST_METHOD': method,
+ 'REMOTE_ADDR': source_ip,
+ 'SERVER_NAME': host,
+ 'SERVER_PORT': str(port),
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ 'wsgi.version': (1, 0),
+ 'wsgi.url_scheme': 'http',
+ 'wsgi.errors': cStringIO.StringIO(),
+ 'wsgi.multithread': True,
+ 'wsgi.multiprocess': True,
+ 'wsgi.input': cStringIO.StringIO(body)}
+ util.put_headers_in_environ(headers, environ)
+ environ['HTTP_HOST'] = host
+ return environ
+
+ def call_backend(self, orig_request, start_response):
+ """Generate API call (from earlier-saved request).
+
+ This calls start_response and returns the response body.
+
+ Args:
+ orig_request: An ApiRequest, the original request from the user.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ A string containing the response body.
+ """
+ method_config, params = self.lookup_rest_method(orig_request)
+ if not method_config:
+ cors_handler = self._create_cors_handler(orig_request)
+ return util.send_wsgi_not_found_response(start_response,
+ cors_handler=cors_handler)
+
+ # Prepare the request for the back end.
+ transformed_request = self.transform_request(
+ orig_request, params, method_config)
+
+ # Check if this call is for the Discovery service. If so, route
+ # it to our Discovery handler.
+ discovery = discovery_service.DiscoveryService(
+ self.config_manager, self._backend)
+ discovery_response = discovery.handle_discovery_request(
+ transformed_request.path, transformed_request, start_response)
+ if discovery_response:
+ return discovery_response
+
+ url = transformed_request.base_path + transformed_request.path
+ transformed_request.headers['Content-Type'] = 'application/json'
+ transformed_environ = self.prepare_backend_environ(
+ orig_request.server, 'POST', url, transformed_request.headers.items(),
+ transformed_request.body, transformed_request.source_ip,
+ orig_request.port)
+
+ # Send the transformed request to the backend app and capture the response.
+ with util.StartResponseProxy() as start_response_proxy:
+ body_iter = self._backend(transformed_environ, start_response_proxy.Proxy)
+ status = start_response_proxy.response_status
+ headers = start_response_proxy.response_headers
+
+ # Get response body
+ body = start_response_proxy.response_body
+ # In case standard WSGI behavior is implemented later...
+ if not body:
+ body = ''.join(body_iter)
+
+ return self.handle_backend_response(orig_request, transformed_request,
+ status, headers, body, method_config,
+ start_response)
+
+ class __CheckCorsHeaders(object):
+ """Track information about CORS headers and our response to them."""
+
+ def __init__(self, request):
+ self.allow_cors_request = False
+ self.origin = None
+ self.cors_request_method = None
+ self.cors_request_headers = None
+
+ self.__check_cors_request(request)
+
+ def __check_cors_request(self, request):
+ """Check for a CORS request, and see if it gets a CORS response."""
+ # Check for incoming CORS headers.
+ self.origin = request.headers[_CORS_HEADER_ORIGIN]
+ self.cors_request_method = request.headers[_CORS_HEADER_REQUEST_METHOD]
+ self.cors_request_headers = request.headers[
+ _CORS_HEADER_REQUEST_HEADERS]
+
+ # Check if the request should get a CORS response.
+ if (self.origin and
+ ((self.cors_request_method is None) or
+ (self.cors_request_method.upper() in _CORS_ALLOWED_METHODS))):
+ self.allow_cors_request = True
+
+ def update_headers(self, headers_in):
+ """Add CORS headers to the response, if needed."""
+ if not self.allow_cors_request:
+ return
+
+ # Add CORS headers.
+ headers = wsgiref.headers.Headers(headers_in)
+ headers[_CORS_HEADER_ALLOW_CREDS] = 'true'
+ headers[_CORS_HEADER_ALLOW_ORIGIN] = self.origin
+ headers[_CORS_HEADER_ALLOW_METHODS] = ','.join(tuple(
+ _CORS_ALLOWED_METHODS))
+ headers[_CORS_HEADER_EXPOSE_HEADERS] = ','.join(tuple(
+ _CORS_EXPOSED_HEADERS))
+ if self.cors_request_headers is not None:
+ headers[_CORS_HEADER_ALLOW_HEADERS] = self.cors_request_headers
+
+ def _create_cors_handler(self, request):
+ return EndpointsDispatcherMiddleware.__CheckCorsHeaders(request)
+
+ def handle_backend_response(self, orig_request, backend_request,
+ response_status, response_headers,
+ response_body, method_config, start_response):
+ """Handle backend response, transforming output as needed.
+
+ This calls start_response and returns the response body.
+
+ Args:
+ orig_request: An ApiRequest, the original request from the user.
+ backend_request: An ApiRequest, the transformed request that was
+ sent to the backend handler.
+ response_status: A string, the status from the response.
+ response_headers: A dict, the headers from the response.
+ response_body: A string, the body of the response.
+ method_config: A dict, the API config of the method to be called.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ A string containing the response body.
+ """
+ # Verify that the response is json. If it isn't treat, the body as an
+ # error message and wrap it in a json error response.
+ for header, value in response_headers:
+ if (header.lower() == 'content-type' and
+ not value.lower().startswith('application/json')):
+ return self.fail_request(orig_request,
+ 'Non-JSON reply: %s' % response_body,
+ start_response)
+
+ self.check_error_response(response_body, response_status)
+
+ # Check if the response from the API was empty. Empty REST responses
+ # generate a HTTP 204.
+ empty_response = self.check_empty_response(orig_request, method_config,
+ start_response)
+ if empty_response is not None:
+ return empty_response
+
+ body = self.transform_rest_response(response_body)
+
+ cors_handler = self._create_cors_handler(orig_request)
+ return util.send_wsgi_response(response_status, response_headers, body,
+ start_response, cors_handler=cors_handler)
+
+ def fail_request(self, orig_request, message, start_response):
+ """Write an immediate failure response to outfile, no redirect.
+
+ This calls start_response and returns the error body.
+
+ Args:
+ orig_request: An ApiRequest, the original request from the user.
+ message: A string containing the error message to be displayed to user.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ A string containing the body of the error response.
+ """
+ cors_handler = self._create_cors_handler(orig_request)
+ return util.send_wsgi_error_response(
+ message, start_response, cors_handler=cors_handler)
+
+ def lookup_rest_method(self, orig_request):
+ """Looks up and returns rest method for the currently-pending request.
+
+ Args:
+ orig_request: An ApiRequest, the original request from the user.
+
+ Returns:
+ A tuple of (method descriptor, parameters), or (None, None) if no method
+ was found for the current request.
+ """
+ method_name, method, params = self.config_manager.lookup_rest_method(
+ orig_request.path, orig_request.request_uri, orig_request.http_method)
+ orig_request.method_name = method_name
+ return method, params
+
+ def transform_request(self, orig_request, params, method_config):
+ """Transforms orig_request to apiserving request.
+
+ This method uses orig_request to determine the currently-pending request
+ and returns a new transformed request ready to send to the backend. This
+ method accepts a rest-style or RPC-style request.
+
+ Args:
+ orig_request: An ApiRequest, the original request from the user.
+ params: A dictionary containing path parameters for rest requests, or
+ None for an RPC request.
+ method_config: A dict, the API config of the method to be called.
+
+ Returns:
+ An ApiRequest that's a copy of the current request, modified so it can
+ be sent to the backend. The path is updated and parts of the body or
+ other properties may also be changed.
+ """
+ method_params = method_config.get('request', {}).get('parameters', {})
+ request = self.transform_rest_request(orig_request, params, method_params)
+ request.path = method_config.get('rosyMethod', '')
+ return request
+
+ def _add_message_field(self, field_name, value, params):
+ """Converts a . delimitied field name to a message field in parameters.
+
+ This adds the field to the params dict, broken out so that message
+ parameters appear as sub-dicts within the outer param.
+
+ For example:
+ {'a.b.c': ['foo']}
+ becomes:
+ {'a': {'b': {'c': ['foo']}}}
+
+ Args:
+ field_name: A string containing the '.' delimitied name to be converted
+ into a dictionary.
+ value: The value to be set.
+ params: The dictionary holding all the parameters, where the value is
+ eventually set.
+ """
+ if '.' not in field_name:
+ params[field_name] = value
+ return
+
+ root, remaining = field_name.split('.', 1)
+ sub_params = params.setdefault(root, {})
+ self._add_message_field(remaining, value, sub_params)
+
+ def _update_from_body(self, destination, source):
+ """Updates the dictionary for an API payload with the request body.
+
+ The values from the body should override those already in the payload, but
+ for nested fields (message objects) the values can be combined
+ recursively.
+
+ Args:
+ destination: A dictionary containing an API payload parsed from the
+ path and query parameters in a request.
+ source: A dictionary parsed from the body of the request.
+ """
+ for key, value in source.items():
+ destination_value = destination.get(key)
+ if isinstance(value, dict) and isinstance(destination_value, dict):
+ self._update_from_body(destination_value, value)
+ else:
+ destination[key] = value
+
+ def transform_rest_request(self, orig_request, params, method_parameters):
+ """Translates a Rest request into an apiserving request.
+
+ This makes a copy of orig_request and transforms it to apiserving
+ format (moving request parameters to the body).
+
+ The request can receive values from the path, query and body and combine
+ them before sending them along to the backend. In cases of collision,
+ objects from the body take precedence over those from the query, which in
+ turn take precedence over those from the path.
+
+ In the case that a repeated value occurs in both the query and the path,
+ those values can be combined, but if that value also occurred in the body,
+ it would override any other values.
+
+ In the case of nested values from message fields, non-colliding values
+ from subfields can be combined. For example, if '?a.c=10' occurs in the
+ query string and "{'a': {'b': 11}}" occurs in the body, then they will be
+ combined as
+
+ {
+ 'a': {
+ 'b': 11,
+ 'c': 10,
+ }
+ }
+
+ before being sent to the backend.
+
+ Args:
+ orig_request: An ApiRequest, the original request from the user.
+ params: A dict with URL path parameters extracted by the config_manager
+ lookup.
+ method_parameters: A dictionary containing the API configuration for the
+ parameters for the request.
+
+ Returns:
+ A copy of the current request that's been modified so it can be sent
+ to the backend. The body is updated to include parameters from the
+ URL.
+ """
+ request = orig_request.copy()
+ body_json = {}
+
+ # Handle parameters from the URL path.
+ for key, value in params.items():
+ # Values need to be in a list to interact with query parameter values
+ # and to account for case of repeated parameters
+ body_json[key] = [value]
+
+ # Add in parameters from the query string.
+ if request.parameters:
+ # For repeated elements, query and path work together
+ for key, value in request.parameters.items():
+ if key in body_json:
+ body_json[key] = value + body_json[key]
+ else:
+ body_json[key] = value
+
+ # Validate all parameters we've merged so far and convert any '.' delimited
+ # parameters to nested parameters. We don't use items since we may
+ # modify body_json within the loop. For instance, 'a.b' is not a valid key
+ # and would be replaced with 'a'.
+ for key, value in body_json.items():
+ current_parameter = method_parameters.get(key, {})
+ repeated = current_parameter.get('repeated', False)
+
+ if not repeated:
+ body_json[key] = body_json[key][0]
+
+ # Order is important here. Parameter names are dot-delimited in
+ # parameters instead of nested in dictionaries as a message field is, so
+ # we need to call transform_parameter_value on them before calling
+ # _add_message_field.
+ body_json[key] = parameter_converter.transform_parameter_value(
+ key, body_json[key], current_parameter)
+ # Remove the old key and try to convert to nested message value
+ message_value = body_json.pop(key)
+ self._add_message_field(key, message_value, body_json)
+
+ # Add in values from the body of the request.
+ if request.body_json:
+ self._update_from_body(body_json, request.body_json)
+
+ request.body_json = body_json
+ request.body = json.dumps(request.body_json)
+ return request
+
+ def check_error_response(self, body, status):
+ """Raise an exception if the response from the backend was an error.
+
+ Args:
+ body: A string containing the backend response body.
+ status: A string containing the backend response status.
+
+ Raises:
+ BackendError if the response is an error.
+ """
+ status_code = int(status.split(' ', 1)[0])
+ if status_code >= 300:
+ raise errors.BackendError(body, status)
+
+ def check_empty_response(self, orig_request, method_config, start_response):
+ """If the response from the backend is empty, return a HTTP 204 No Content.
+
+ Args:
+ orig_request: An ApiRequest, the original request from the user.
+ method_config: A dict, the API config of the method to be called.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ If the backend response was empty, this returns a string containing the
+ response body that should be returned to the user. If the backend
+ response wasn't empty, this returns None, indicating that we should not
+ exit early with a 204.
+ """
+ response_config = method_config.get('response', {}).get('body')
+ if response_config == 'empty':
+ # The response to this function should be empty. We should return a 204.
+ # Note that it's possible that the backend returned something, but we'll
+ # ignore it. This matches the behavior in the Endpoints server.
+ cors_handler = self._create_cors_handler(orig_request)
+ return util.send_wsgi_no_content_response(start_response, cors_handler)
+
+ def transform_rest_response(self, response_body):
+ """Translates an apiserving REST response so it's ready to return.
+
+ Currently, the only thing that needs to be fixed here is indentation,
+ so it's consistent with what the live app will return.
+
+ Args:
+ response_body: A string containing the backend response.
+
+ Returns:
+ A reformatted version of the response JSON.
+ """
+ body_json = json.loads(response_body)
+ return json.dumps(body_json, indent=1, sort_keys=True)
+
+ def _handle_request_error(self, orig_request, error, start_response):
+ """Handle a request error, converting it to a WSGI response.
+
+ Args:
+ orig_request: An ApiRequest, the original request from the user.
+ error: A RequestError containing information about the error.
+ start_response: A function with semantics defined in PEP-333.
+
+ Returns:
+ A string containing the response body.
+ """
+ headers = [('Content-Type', 'application/json')]
+ status_code = error.status_code()
+ body = error.rest_error()
+
+ response_status = '%d %s' % (status_code,
+ http_client.responses.get(status_code,
+ 'Unknown Error'))
+ cors_handler = self._create_cors_handler(orig_request)
+ return util.send_wsgi_response(response_status, headers, body,
+ start_response, cors_handler=cors_handler)