添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Howdy! In the previous Part of the series, we learned how we can add authentication and authorization . In this part, we are going to learn how we can make our flask application more resilient to errors, and how to send a proper error message to the client.

When something goes wrong in a computer program, the program throws a specific Exception giving some hint to the user what went wrong. In our application when something goes wrong e.g user tries to create another account with the already used email address, they get an Internal Server Error and the client has no idea what they did wrong. So, to solve such issues we are going to use Exception Handling to catch such exceptions and send a proper error message to the client indicating what went wrong.

We are going to use a really useful feature of flask-restful which lets us define Custom Error Messages .

Let's create a new file errors.py inside the resources folder and add the following code:

cd resources
touch errors.py
     "SchemaValidationError": {
         "message": "Request is missing required fields",
         "status": 400
     "MovieAlreadyExistsError": {
         "message": "Movie with given name already exists",
         "status": 400
     "UpdatingMovieError": {
         "message": "Updating movie added by other is forbidden",
         "status": 403
     "DeletingMovieError": {
         "message": "Deleting movie added by other is forbidden",
         "status": 403
     "MovieNotExistsError": {
         "message": "Movie with given id doesn't exists",
         "status": 400
     "EmailAlreadyExistsError": {
         "message": "User with given email address already exists",
         "status": 400
     "UnauthorizedError": {
         "message": "Invalid username or password",
         "status": 401

As you can see first we have extended the Exception class to create different custom exceptions and then we created an errors dictionary, which contains the error message and status codes for each exception. Now, we need to add these errors to the flask-restful Api class.

Update app.py to import recently created errors dictionary and add this as a parameter to Api class.

#~/movie-bag/app.py
from database.db import initialize_db
 from flask_restful import Api
 from resources.routes import initialize_routes
+from resources.errors import errors
 app = Flask(__name__)
 app.config.from_envvar('ENV_FILE_LOCATION')
-api = Api(app)
+api = Api(app, errors=errors)
 bcrypt = Bcrypt(app)
 jwt = JWTManager(app)

Finally, we are ready to perform some exception handling in our application. Update movie.py view functions according to the following:

#~/movie-bag/resources/movie.py
 from flask import Response, request
 from database.models import Movie, User
 from flask_jwt_extended import jwt_required, get_jwt_identity
 from flask_restful import Resource
+from mongoengine.errors import FieldDoesNotExist, \
+NotUniqueError, DoesNotExist, ValidationError, InvalidQueryError
+from resources.errors import SchemaValidationError, +MovieAlreadyExistsError, \
+InternalServerError, UpdatingMovieError, DeletingMovieError, +MovieNotExistsError
 class MoviesApi(Resource):
     def get(self):
@@ -11,32 +15,57 @@ class MoviesApi(Resource):
     @jwt_required
     def post(self):
-        user_id = get_jwt_identity()
-        body = request.get_json()
-        user = User.objects.get(id=user_id)
-        movie =  Movie(**body, added_by=user)
-        movie.save()
-        user.update(push__movies=movie)
-        user.save()
-        id = movie.id
-        return {'id': str(id)}, 200
+        try:
+            user_id = get_jwt_identity()
+            body = request.get_json()
+            user = User.objects.get(id=user_id)
+            movie =  Movie(**body, added_by=user)
+            movie.save()
+            user.update(push__movies=movie)
+            user.save()
+            id = movie.id
+            return {'id': str(id)}, 200
+        except (FieldDoesNotExist, ValidationError):
+            raise SchemaValidationError
+        except NotUniqueError:
+            raise MovieAlreadyExistsError
+        except Exception as e:
+            raise InternalServerError
 class MovieApi(Resource):
     @jwt_required
     def put(self, id):
-        user_id = get_jwt_identity()
-        movie = Movie.objects.get(id=id, added_by=user_id)
-        body = request.get_json()
-        Movie.objects.get(id=id).update(**body)
-        return '', 200
+        try:
+            user_id = get_jwt_identity()
+            movie = Movie.objects.get(id=id, added_by=user_id)
+            body = request.get_json()
+            Movie.objects.get(id=id).update(**body)
+            return '', 200
+        except InvalidQueryError:
+            raise SchemaValidationError
+        except DoesNotExist:
+            raise UpdatingMovieError
+        except Exception:
+            raise InternalServerError       
     @jwt_required
     def delete(self, id):
-        user_id = get_jwt_identity()
-        movie = Movie.objects.get(id=id, added_by=user_id)
-        movie.delete()
-        return '', 200
+        try:
+            user_id = get_jwt_identity()
+            movie = Movie.objects.get(id=id, added_by=user_id)
+            movie.delete()
+            return '', 200
+        except DoesNotExist:
+            raise DeletingMovieError
+        except Exception:
+            raise InternalServerError
     def get(self, id):
-        movies = Movie.objects.get(id=id).to_json()
-        return Response(movies, mimetype="application/json", status=200)
+        try:
+            movies = Movie.objects.get(id=id).to_json()
+            return Response(movies, mimetype="application/json", status=200)
+        except DoesNotExist:
+            raise MovieNotExistsError
+        except Exception:
+            raise InternalServerError
      user_id = get_jwt_identity()
      body = request.get_json()
      user = User.objects.get(id=user_id)
      movie =  Movie(**body, added_by=user)
      movie.save()
      user.update(push__movies=movie)
      user.save()
      id = movie.id
      return {'id': str(id)}, 200
  except (FieldDoesNotExist, ValidationError):
      raise SchemaValidationError
  except NotUniqueError:
      raise MovieAlreadyExistsError
  except Exception as e:
      raise InternalServerError

Here you can see we have wrapped the whole view opetations in try...except block. We have performed an exception chaining, so that when we get any exception we throw the exception which we have defined in errors.py and flask-restful generates a response based on the values we defined in errors dictionary.

When there is FieldDoesNotExist exception or ValidationError exception from mongoengine we raise SchemaValidationError exception which tells the client that their request JSON is invalid. Similarly, when a user tries to create a movie with the name which already exists mongoengine throws NotUniqueError exception and by catching that exception we raise MovieAlreadyExistsError which tells the user that the movie name already exists.

And lastly, if we get an exception that we have not expected then we throw an InternalServerError.

Let's add similar exception handling to our auth.py

#~/movie-bag/resources/auth.py
 from database.models import User
 from flask_restful import Resource
 import datetime
+from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist
+from resources.errors import SchemaValidationError, EmailAlreadyExistsError, UnauthorizedError, \
+InternalServerError
 class SignupApi(Resource):
     def post(self):
-        body = request.get_json()
-        user =  User(**body)
-        user.hash_password()
-        user.save()
-        id = user.id
-        return {'id': str(id)}, 200
+        try:
+            body = request.get_json()
+            user =  User(**body)
+            user.hash_password()
+            user.save()
+            id = user.id
+            return {'id': str(id)}, 200
+        except FieldDoesNotExist:
+            raise SchemaValidationError
+        except NotUniqueError:
+            raise EmailAlreadyExistsError
+        except Exception as e:
+            raise InternalServerError
 class LoginApi(Resource):
     def post(self):
-        body = request.get_json()
-        user = User.objects.get(email=body.get('email'))
-        authorized = user.check_password(body.get('password'))
-        if not authorized:
-            return {'error': 'Email or password invalid'}, 401
-        expires = datetime.timedelta(days=7)
-        access_token = create_access_token(identity=str(user.id), expires_delta=expires)
-        return {'token': access_token}, 200
+        try:
+            body = request.get_json()
+            user = User.objects.get(email=body.get('email'))
+            authorized = user.check_password(body.get('password'))
+            if not authorized:
+                raise UnauthorizedError
+            expires = datetime.timedelta(days=7)
+            access_token = create_access_token(identity=str(user.id), expires_delta=expires)
+            return {'token': access_token}, 200
+        except (UnauthorizedError, DoesNotExist):
+            raise UnauthorizedError
+        except Exception as e:
+            raise InternalServerError

That's it, people. Now, when there is an error in our application we get a proper error message with relevant status code.

Let's try creating a user with /api/auth/signup with any email address, let's say [email protected]. Now again try to create another user with the same email address. We get a response like this:
"message": "User with given email address already exists", "status": 400 In the next part of the series, we are going to learn how to perform a password reset in our application.

Until then happy coding 😊

very good article! thanks

please update the above link, you current references a 404 this is the new link:

flask-restful.readthedocs.io/en/la...

a minor addition:

in the new docs its says:

"Note: Custom Exceptions must have HTTPException as the base Exception."

if i do change the errors in the dictionary to extends HTTPException instead of Exception i get:

   ~  curl -v -X POST --header 'Content-Type: application/json' -d '{"password":"Password20", "email":"[email protected]", "phone":"016553225644"}' 'http://127.0.0.1:8888/user/register'                                                               ✔  22:30:26 
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8888...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8888 (#0)
> POST /user/register HTTP/1.1
> Host: 127.0.0.1:8888
> User-Agent: curl/7.65.3
> Accept: */*
> Content-Type: application/json
> Content-Length: 80
* upload completely sent off: 80 out of 80 bytes
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: text/html
< Content-Length: 121
< Server: Werkzeug/0.16.0 Python/3.7.6
< Date: Mon, 23 Mar 2020 21:30:28 GMT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>None Unknown Error</title>
<h1>Unknown Error</h1>
* Closing connection 0
          

[UPDATE]: I've found another working workaround (I think you too ;) ) : github.com/vimalloc/flask-jwt-exte...

Very nice article series! Thanks a lot for the great stuff!
One question: have you tried accessing the protected endpoints without passing the Authorization header? According to my test, it will throw a 500 instead of expected 401. It seems that the error handling framework could not catch the internal NoAuthorizationError from jwt_extended properly.
I tried one suggestion from github.com/flask-restful/flask-res... . However, it will make the situation even worse: NoAuthorizationError can be properly returned, but all other self-defined errors could not be caught. Do you have a better solution to that?

Haha glad that you found it ;) I asked the question there while writing the article and got the answer back after I have published the article.
Thanks for editing your comment to help others :)

Great Tutorials, thanks.
This exceptions didn't work to me, until i replaced "Exception" with "HTTPException".
maybe there was a change ....
see here the last line in the page:
flask-restful.readthedocs.io/en/la...

I was having that trouble but if you turn debug off i.e. app.run() rather than app.run(debug=True)
it works for me

Hi. Great articles. Cant wait till you make a frontend implementation series (as promised in ;) )
How come you havn't made a "ExpiredTokenError" in errors.py when it's raised in reset_password.py?

Oh yeah, I forgot about that promise ;)
Hopefully, I can start the frontend series soon. Before that, I want to make some improvements to this backend like adding images.

About ExpiredTokenError that's a mistake, I should add ExpiredTokenError in errors.py as well.

How to import in case of 2 different error modules, for example
resource_errors and user_errors?

I tried as below but its not working

from resources.errors import resource_errors
from user.errors import user_errors
api = Api(app, errors={'user_errors':user_errors, 'resource_errors':resource_errors})

Will be great if you can suggest me what i am doing wrong?

I Found a solution,

the parameter error accepts only dictionary, so merge the different error dictionary into one and then pass that dictionary
example:
collective_errors = {**user_errors, **resource_errors}
api = Api(app, errors=collective_errors)

Great implementation!

One quick note, you can avoid handling your exceptions with try-catch by extending the HTTPException class.
FlaskRESTful will handle these exceptions for you.

Secondly, you can avoid defining a 'pass' in error classes like InternalServerError, seeing the it already exists in Werkzeug.
FlaskRESTful will access your errors dictionary and display your JSON.

Lovely read once again, you improved my understanding.
Cheers.

Built on Forem — the open source software that powers DEV and other inclusive communities.

Made with love and Ruby on Rails. DEV Community © 2016 - 2024.