Im having some trouble with designing the exception classes for a Python web API. What I would like to do is have various exceptions set up with some default error codes/messages, but also allow the flexibility of creating a custom one.
Take the following code:
class APIException(Exception):
def __init__(self):
super().__init__(self.message)
#property
def message(self) -> str:
raise NotImplementedError
#property
def code(self) -> int:
raise NotImplementedError
#property
def response(self):
return {"error": self.message}, self.code
class UnknownException(APIException):
message = "An unknown error occurred."
code = 500
class UnauthorizedException(APIException):
message = "Unauthorized"
code = 401
This allows me to do things like raise UnauthorizedException, which work fine.
However, what I would like to be able to do is to raise arbitrary API exceptions, like raise APIException("This is a custom error", 404). Raising a set exception with arguments and raising an APIException without arguments do not need to be supported; I will not be raising them like that.
It doesn't seem I can do this cleanly with the way I have designed the inheritance above. I have tried other various approaches but none seem to be as clean as the example above.
What would be the best way to go about doing this sort of thing?
Have your APIException constructor take arguments, and have the subclasses implement constructors that provide those arguments:
class APIException(Exception):
def __init__(self, message, code):
super().__init__(message)
self.message = message
self.code = code
#property
def response(self):
return {"error": self.message}, self.code
class UnknownException(APIException):
def __init__():
super().__init__("An unknown error occurred.", 500)
class UnauthorizedException(APIException):
def __init__():
super().__init__("Unauthorized", 401)
I use tornado and a jwt decorator as below:
def jwtauth(handler_class):
"""
Tornado JWT Auth Decorator
"""
def wrap_execute(handler_execute):
def require_auth(handler, kwargs):
auth = handler.request.headers.get(AUTHORIZATION_HEADER)
if auth:
parts = auth.split()
if not is_valid_header(parts):
return_header_error(handler)
token = parts[1]
try:
result = jwt.decode(
token,
SECRET_KEY,
options=jwt_options
)
except Exception as err:
return_auth_error(handler, str(err))
else:
handler._transforms = []
handler.write(MISSING_AUTHORIZATION_KEY)
handler.finish()
return result
def _execute(self, transforms, *args, **kwargs):
try:
result = require_auth(self, kwargs)
except Exception:
return False
return handler_execute(self, transforms, *args, **kwargs)
return _execute
handler_class._execute = wrap_execute(handler_class._execute)
return handler_class
#jwtauth
class MyHandler(tornado.web.RequestHandler):
def post(self):
unit = json.loads(self.request.body.decode('utf-8'))
# TODO:
# get the result from jwtauth decorator and use it here
print(result) # The result from jwtauth
Now, I'd like to get the jwt decode result and pass into MyHandler for further verification. Can I do it? I checked most of the comment that I can pass the parameter to a decorator but I cannot get from it. Is is possible to pass the jwtauth result to my function?
A class decorator takes your class and spits out a new version of your class (usually with some features added to it). In this case, the #jwtauth decorator takes your class and spits out a new class that makes sure that each request is checked for a valid JWT token in the authorization header. tornado.web.RequestHandler._execute internally calls post. The current behavior is that if the JWT token auth fails, then post will never be called.
In short, you probably want to just raise an error below instead of returning False.
try:
result = require_auth(self, kwargs)
except Exception:
return False
If you need to add more logic about what kind of error to raise, then you probably want to pass that into the decorator along with the class.
I want to define custom error handling for a Flask-restful API.
The suggested approach in the documentation here is to do the following:
errors = {
'UserAlreadyExistsError': {
'message': "A user with that username already exists.",
'status': 409,
},
'ResourceDoesNotExist': {
'message': "A resource with that ID no longer exists.",
'status': 410,
'extra': "Any extra information you want.",
},
}
app = Flask(__name__)
api = flask_restful.Api(app, errors=errors)
Now I find this format pretty attractive but I need to specify more parameters when some exception happens. For example, when encountering ResourceDoesNotExist, I want to specify what id does not exist.
Currently, I'm doing the following:
app = Flask(__name__)
api = flask_restful.Api(app)
class APIException(Exception):
def __init__(self, code, message):
self._code = code
self._message = message
#property
def code(self):
return self._code
#property
def message(self):
return self._message
def __str__(self):
return self.__class__.__name__ + ': ' + self.message
class ResourceDoesNotExist(APIException):
"""Custom exception when resource is not found."""
def __init__(self, model_name, id):
message = 'Resource {} {} not found'.format(model_name.title(), id)
super(ResourceNotFound, self).__init__(404, message)
class MyResource(Resource):
def get(self, id):
try:
model = MyModel.get(id)
if not model:
raise ResourceNotFound(MyModel.__name__, id)
except APIException as e:
abort(e.code, str(e))
When called with an id that doesn't exist MyResource will return the following JSON:
{'message': 'ResourceDoesNotExist: Resource MyModel 5 not found'}
This works fine but I'd like to use to Flask-restful error handling instead.
According to the docs
Flask-RESTful will call the handle_error() function on any 400 or 500 error that happens on a Flask-RESTful route, and leave other routes alone.
You can leverage this to implement the required functionality. The only downside is having to create a custom Api.
class CustomApi(flask_restful.Api):
def handle_error(self, e):
flask_restful.abort(e.code, str(e))
If you keep your defined exceptions, when an exception occurs, you'll get the same behaviour as
class MyResource(Resource):
def get(self, id):
try:
model = MyModel.get(id)
if not model:
raise ResourceNotFound(MyModel.__name__, id)
except APIException as e:
abort(e.code, str(e))
Instead of attaching errors dict to Api, I am overriding handle_error method of Api class to handle exceptions of my application.
# File: app.py
# ------------
from flask import Blueprint, jsonify
from flask_restful import Api
from werkzeug.http import HTTP_STATUS_CODES
from werkzeug.exceptions import HTTPException
from view import SomeView
class ExtendedAPI(Api):
"""This class overrides 'handle_error' method of 'Api' class ,
to extend global exception handing functionality of 'flask-restful'.
"""
def handle_error(self, err):
"""It helps preventing writing unnecessary
try/except block though out the application
"""
print(err) # log every exception raised in the application
# Handle HTTPExceptions
if isinstance(err, HTTPException):
return jsonify({
'message': getattr(
err, 'description', HTTP_STATUS_CODES.get(err.code, '')
)
}), err.code
# If msg attribute is not set,
# consider it as Python core exception and
# hide sensitive error info from end user
if not getattr(err, 'message', None):
return jsonify({
'message': 'Server has encountered some error'
}), 500
# Handle application specific custom exceptions
return jsonify(**err.kwargs), err.http_status_code
api_bp = Blueprint('api', __name__)
api = ExtendedAPI(api_bp)
# Routes
api.add_resource(SomeView, '/some_list')
Custom exceptions can be kept in separate file, like:
# File: errors.py
# ---------------
class Error(Exception):
"""Base class for other exceptions"""
def __init__(self, http_status_code:int, *args, **kwargs):
# If the key `msg` is provided, provide the msg string
# to Exception class in order to display
# the msg while raising the exception
self.http_status_code = http_status_code
self.kwargs = kwargs
msg = kwargs.get('msg', kwargs.get('message'))
if msg:
args = (msg,)
super().__init__(args)
self.args = list(args)
for key in kwargs.keys():
setattr(self, key, kwargs[key])
class ValidationError(Error):
"""Should be raised in case of custom validations"""
And in the views exceptions can be raised like:
# File: view.py
# -------------
from flask_restful import Resource
from errors import ValidationError as VE
class SomeView(Resource):
def get(self):
raise VE(
400, # Http Status code
msg='some error message', code=SomeCode
)
Like in view, exceptions can actually be raised from any file in the app which will be handled by the ExtendedAPI handle_error method.
I've used the Blueprint to work with the flask-restful, and I've found that the solution #billmccord and #cedmt provided on the issue not worked for this case, because the Blueprint don't have the handle_exception and handle_user_exception functions.
My workaround is that enhance the function handle_error of the Api, if the "error handler" of the "Exception" have been registered, just raise it, the "error handler" registered on app will deal with that Exception, or the Exception will be passed to the "flask-restful" controlled "custom error handler".
class ImprovedApi(Api):
def handle_error(self, e):
for val in current_app.error_handler_spec.values():
for handler in val.values():
registered_error_handlers = list(filter(lambda x: isinstance(e, x), handler.keys()))
if len(registered_error_handlers) > 0:
raise e
return super().handle_error(e)
api_entry = ImprovedApi(api_entry_bp)
BTW, seems the flask-restful had been deprecated...
I faced the same issue too and after extending flask-restful.Api I realized that
you really don't need to extend the flask-restful.Api
you can easily do this by inheriting from werkzeug.exceptions.HTTPException and solve this issue
app = Flask(__name__)
api = flask_restful.Api(app)
from werkzeug.exceptions import HTTPException
class APIException(HTTPException):
def __init__(self, code, message):
super().__init__()
self.code = code
self.description = message
class ResourceDoesNotExist(APIException):
"""Custom exception when resource is not found."""
def __init__(self, model_name, id):
message = 'Resource {} {} not found'.format(model_name.title(), id)
super().__init__(404, message)
class MyResource(Resource):
def get(self, id):
try:
model = MyModel.get(id)
if not model:
raise ResourceNotFound(MyModel.__name__, id)
except APIException as e:
abort(e.code, str(e))
I'm trying to make what should be a really simple validator using WTForms - ensuring that input is a) present and b) a number of some type (float or int are both OK). I wrote the following class and added it to the validators.py library in WTForms:
class MyValidator(object):
def __init__(self, message=None):
self.message = message
def __call__(self, form, field):
if not field.data:
if self.message is None:
message = field.gettext('This field is required.')
return ValidationError(self.message)
try:
val = float(field.data)
except NameError:
self.message = ('Input must be a number')
raise ValidationError(self.message)
return field.data
And added it to my form:
class foo(Form):
bar = IntegerField('foo',[validators.MyValidator()])
This causes my Flask app to crash when I get to validation stage (if not foo.validate, where foo is an instance of the foo class), with this traceback:
File "C:\Users\~~\AppData\Local\Programs\Python\Python35\lib\site-packages\wtforms\validators.py", line 158, in __call__
val = float(field.data)
TypeError: float() argument must be a string or a number, not 'NoneType'
This is pretty annoying. How do I just return False? Am I missing something obvious?
OK, found my mistake. It was embarrassingly simple, and my instinct was to delete the question, but maybe the code will be helpful for someone else in the future.
The exception line should of course read:
except (TypeError, NameError):
I have a problem with decorator. I'm trying to write my own decorator with optional argument.
This is how its done now:
def CheckPremissions(manager=1):
def wrap(func):
def wrapper(request, *args, **kwargs):
if request.user.is_anonymous():
return HttpResponseRedirect(reverse('login'))
logged_user = getRelatedWorker(request.user)
if (logged_user == None):
return HttpResponseRedirect('accounts/no_worker_error.html')
if self.manager != 0:
try:
dzial = Dzial.objects.get(kierownik=logged_user)
except Dzial.DoesNotExist:
isManager = False
else:
isManager = True
if not isManager:
return HttpResponseRedirect('accounts/denied_logged.html')
return func(request, *args, **kwargs)
return wrapper
return wrap
Code is looking good (for me), but when I use a decorator, I'm getting following error:
Environment:
Request Method: GET
Request URL: http://127.0.0.1:8080/applications/show
Django Version: 1.4.1
Python Version: 2.7.3
Traceback:
File "/home/marcin/projekt/lib/python2.7/site-packages/django/core/handlers/base.py" in get_response
188. response = middleware_method(request, response)
File "/home/marcin/projekt/lib/python2.7/site-packages/django/middleware/common.py" in process_response
94. if response.status_code == 404:
Exception Type: AttributeError at /applications/show
Exception Value: 'function' object has no attribute 'status_code'
What am I doing wrong?
I suspect you are applying the decorator incorrectly. You need to call it to specify the manager parameter:
#CheckPremissions()
def someview(request):
pass
or to specify it explicitly:
#CheckPremissions(manager=0)
def someview(request):
pass
You have a different problem in your decorator as well; you refer to self.manager in the code:
if self.manager != 0:
but this is no instance, and there is no self parameter. I think you meant:
if manager:
(where you can test for the variable to be non-zero by treating it as a boolean). Oh, and you may want to fix the spelling of the decorator; you probably meant CheckPermissions instead. :-)