blob: 0248c505962d3c244b8aa979ce95efea73c71691 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# 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.
#
"""Error handling and exceptions used in the local Cloud Endpoints server."""
import json
import logging
from google.appengine.tools.devappserver2.endpoints import generated_error_info
__all__ = ['BackendError',
'BasicTypeParameterError',
'EnumRejectionError',
'InvalidParameterError',
'RequestError',
'RequestRejectionError']
_INVALID_ENUM_TEMPLATE = 'Invalid string value: %r. Allowed values: %r'
_INVALID_BASIC_PARAM_TEMPLATE = 'Invalid %s value: %r.'
class RequestError(Exception):
"""Base class for errors that happen while processing a request."""
def status_code(self):
"""HTTP status code number associated with this error.
Subclasses must implement this, returning an integer with the status
code number for the error.
Example: 400
Raises:
NotImplementedError: Subclasses must override this function.
"""
raise NotImplementedError
def message(self):
"""Text message explaining the error.
Subclasses must implement this, returning a string that explains the
error.
Raises:
NotImplementedError: Subclasses must override this function.
"""
raise NotImplementedError
def reason(self):
"""Get the reason for the error.
Error reason is a custom string in the Cloud Endpoints server. When
possible, this should match the reason that the live server will generate,
based on the error's status code. If this returns None, the error formatter
will attempt to generate a reason from the status code.
Returns:
None, by default. Subclasses can override this if they have a specific
error reason.
"""
raise NotImplementedError
def domain(self):
"""Get the domain for this error.
Returns:
The string 'global' by default. Subclasses can override this if they have
a different domain.
"""
return 'global'
def extra_fields(self):
"""Return a dict of extra fields to add to the error response.
Some errors have additional information. This provides a way for subclasses
to provide that information.
Returns:
None, by default. Subclasses can return a dict with values to add
to the error response.
"""
return None
def __format_error(self, error_list_tag):
"""Format this error into a JSON response.
Args:
error_list_tag: A string specifying the name of the tag to use for the
error list.
Returns:
A dict containing the reformatted JSON error response.
"""
error = {'domain': self.domain(),
'reason': self.reason(),
'message': self.message()}
error.update(self.extra_fields() or {})
return {'error': {error_list_tag: [error],
'code': self.status_code(),
'message': self.message()}}
def rest_error(self):
"""Format this error into a response to a REST request.
Returns:
A string containing the reformatted error response.
"""
error_json = self.__format_error('errors')
return json.dumps(error_json, indent=1, sort_keys=True)
def rpc_error(self):
"""Format this error into a response to a JSON RPC request.
Returns:
A dict containing the reformatted JSON error response.
"""
return self.__format_error('data')
class RequestRejectionError(RequestError):
"""Base class for invalid/rejected requests.
To be raised when parsing the request values and comparing them against the
generated discovery document.
"""
def status_code(self):
return 400
class InvalidParameterError(RequestRejectionError):
"""Base class for invalid parameter errors.
Child classes only need to implement the message() function.
"""
def __init__(self, parameter_name, value):
"""Constructor for InvalidParameterError.
Args:
parameter_name: String; the name of the parameter which had a value
rejected.
value: The actual value passed in for the parameter. Usually string.
"""
super(InvalidParameterError, self).__init__()
self.parameter_name = parameter_name
self.value = value
def reason(self):
"""Returns the server's reason for this error.
Returns:
A string containing a short error reason.
"""
return 'invalidParameter'
def extra_fields(self):
"""Returns extra fields to add to the error response.
Returns:
A dict containing extra fields to add to the error response.
"""
return {'locationType': 'parameter',
'location': self.parameter_name}
class BasicTypeParameterError(InvalidParameterError):
"""Request rejection exception for basic types (int, float)."""
def __init__(self, parameter_name, value, type_name):
"""Constructor for BasicTypeParameterError.
Args:
parameter_name: String; the name of the parameter which had a value
rejected.
value: The actual value passed in for the enum. Usually string.
type_name: Descriptive name of the data type expected.
"""
super(BasicTypeParameterError, self).__init__(parameter_name, value)
self.type_name = type_name
def message(self):
"""A descriptive message describing the error."""
return _INVALID_BASIC_PARAM_TEMPLATE % (self.type_name, self.value)
class EnumRejectionError(InvalidParameterError):
"""Custom request rejection exception for enum values."""
def __init__(self, parameter_name, value, allowed_values):
"""Constructor for EnumRejectionError.
Args:
parameter_name: String; the name of the enum parameter which had a value
rejected.
value: The actual value passed in for the enum. Usually string.
allowed_values: List of strings allowed for the enum.
"""
super(EnumRejectionError, self).__init__(parameter_name, value)
self.allowed_values = allowed_values
def message(self):
"""A descriptive message describing the error."""
return _INVALID_ENUM_TEMPLATE % (self.value, self.allowed_values)
class BackendError(RequestError):
"""Exception raised when the backend SPI returns an error code."""
def __init__(self, response):
super(BackendError, self).__init__()
# Convert backend error status to whatever the live server would return.
status_code = self._get_status_code(response.status)
self._error_info = generated_error_info.get_error_info(status_code)
try:
error_json = json.loads(response.content)
self._message = error_json.get('error_message')
except TypeError:
self._message = response.content
def _get_status_code(self, http_status):
"""Get the HTTP status code from an HTTP status string.
Args:
http_status: A string containing a HTTP status code and reason.
Returns:
An integer with the status code number from http_status.
"""
try:
return int(http_status.split(' ', 1)[0])
except TypeError:
logging.warning('Unable to find status code in HTTP status %r.',
http_status)
return 500
def status_code(self):
"""Return the HTTP status code number for this error.
Returns:
An integer containing the status code for this error.
"""
return self._error_info.http_status
def message(self):
"""Return a descriptive message for this error.
Returns:
A string containing a descriptive message for this error.
"""
return self._message
def reason(self):
"""Return the short reason for this error.
Returns:
A string with the reason for this error.
"""
return self._error_info.reason
def domain(self):
"""Return the remapped domain for this error.
Returns:
A string containing the remapped domain for this error.
"""
return self._error_info.domain