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)