SoFunction
Updated on 2024-10-30

Response error handling in flask and how errorhandler is applied

flask response error handling and errorhandler application

@(404)
def page_not_found(error):
    return render_template(''),404

When there is a request error, you do not necessarily need to set up a redirection route, but only define an errorhandler corresponding to the error page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>404</h1>
<h1>I'm sorry.!</h1>
<p>The page you are visiting does not exist</p>
</body>
</html>!

flask learning notes: error handling

1. Make preparations

  • Go to the project home directory
  • Activate the virtual environment

2. Error handling in Flask

Log in to your account, click on the Edit Profile page, try to change your username to one that already exists, and then you will see the screen display "Internal Server Error".

Now, take a look at the command line terminal and you can see the error stack trace, stack traces are very useful in error debugging because they show the sequence of calls in that stack all the way down to the line that produced the error:

(venv) $ flask run
 * Serving Flask app "microblog"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
[2017-09-14 22:40:02,027] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
  File "/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/", line 1182, in _execute_context
    context)
  File "/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/", line 470, in do_execute
    (statement, parameters)
: UNIQUE constraint failed:

The stack trace points out where the bug is coming from; the application allows the user to change the username, but does not verify that the new username chosen by the user does not conflict with a user already in the system. The error comes from SQLAlchemy, which tries to write the new username to the database, but the write is rejected because the username field is set to unique=True.

The default error page is now ugly and doesn't match the layout of the whole app.

3. Commissioning mode

In a production server, errors are handled well as above, and if an error occurs, the user is presented with a generalized error page, while detailed error details are output to the server process or log file.

But when you are developing your application, you can turn on debug mode and Flask will output a very good debugger in the browser. You can activate debug mode by closing the app first and setting the following environment variables:

(venv) $ export FLASK_DEBUG=1

Windows uses set to set.

After setting FLASK_DEBUG, restart the application and the terminal output will be different from what you saw before:

(venv) microblog2 $ flask run
 * Serving Flask app "microblog"
 * Forcing debug mode on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 177-562-960

Now, try to make the program run again and look at the interactive debugger in the browser.

The debugger allows you to expand each stack frame and view the source code of the response. You can also open a Python prompt on any frame and execute any valid Python expression, such as checking the value of a variable.

The debugger allows the user to remotely execute code from the server, so it's advantageous for just users who want to infiltrate the application. As an added security measure, the debugger allowed in the browser is initially locked down and will ask for a PIN code the first time it is used, as you can see in the output of the flask run command.

Another important feature of debug mode is reloader, which is a useful development feature. If you run flask run while debug mode is on, modify the source file and save it, the application will be reloaded automatically.

4. Customizing error pages

Flask provides a mechanism to enable applications to install their own error pages instead of the default boring page. For example, let's define a 404 error page and a 500 error page, which are the two most common errors. Define other error pages in the same way.

The error manager can be customized with the @errorhandler decorator. Create a new app/ module.

app/

from flask import render_template
from app import app, db
 
@(404)
def not_found_error(error):
    return render_template(''), 404
 
@(500)
def internal_error(error):
    ()
    return render_template(''), 500

The error function works in a similar way to the view function. Both errors return the contents of their respective templates. Notice that both functions return a second value in addition to the template, which is the error code. In none of the previously created view functions did I explicitly define the second value because the default value is 200 (for a successful response). And these two are error pages, so I want that to be reflected in the response.

A database error will call a 500 error. A user name rename in this example would cause this error. To ensure that database transaction errors do not interfere with template-triggered database accesses, the program performs a transaction rollback.

Here is the template for 404 errors:

app/templates/

{% extends "" %}
 
{% block content %}
    <h1>File Not Found</h1>
    <p><a href="{{ url_for('index') }}" rel="external nofollow"  rel="external nofollow" >Back</a></p>
{% endblock %}

That's 500 wrong:

app/templates/

{% extends "" %}
 
{% block content %}
    <h1>An unexpected error has occurred</h1>
    <p>The administrator has been notified. Sorry for the inconvenience!</p>
    <p><a href="{{ url_for('index') }}" rel="external nofollow"  rel="external nofollow" >Back</a></p>
{% endblock %}

Both templates inherit from the template.

After the app/ module is imported into the __init__.py file for the application instance.

app/__init__.py

# ...
 
from app import routes, models, errors

Set FLASK_DEBUG=0 in the terminal and try triggering the username rename error again and you'll see a friendlier error page.

5. Sending errors by mail

The error handling mechanism provided by Flask has a problem, that is, there is no hint, only in the terminal output stack trace, development is fine, but if the application is deployed to the production server, no one will stare at the output, so we have to think of individual ways.

The first solution is to send an email to notify the administrator when an error is triggered, with the body of the email containing the stack trace.

The first step is to add the mail service information to the configuration file:

class Config(object):
    # ...
    MAIL_SERVER = ('MAIL_SERVER')
    MAIL_PORT = int(('MAIL_PORT') or 25)
    MAIL_USE_TLS = ('MAIL_USE_TLS') is not None
    MAIL_USERNAME = ('MAIL_USERNAME')
    MAIL_PASSWORD = ('MAIL_PASSWORD')
    ADMINS = ['your-email@']

The mail configuration variables contain the server and port, a boolean value to enable encrypted connections, and optionally a username and password. All five configuration variables are loaded from the environment variables. If the mail server is not set in the environment, I disable the mail error prompt. The mail port can also be set in the environment variable; if it is not set, port 25 is used.The ADMIN configuration variable represents the e-mail address used to receive error reports.

Flask writes logs using Python's logging package, which can be used to send logs by email. All I have to do is add the SMTPHandler instance to the Flask logger object, which is :

app/__init__.py

import logging
from  import SMTPHandler
 
# ...
 
if not :
    if ['MAIL_SERVER']:
        auth = None
        if ['MAIL_USERNAME'] or ['MAIL_PASSWORD']:
            auth = (['MAIL_USERNAME'], ['MAIL_PASSWORD'])
        secure = None
        if ['MAIL_USE_TLS']:
            secure = ()
        mail_handler = SMTPHandler(
            mailhost=(['MAIL_SERVER'], ['MAIL_PORT']),
            fromaddr='no-reply@' + ['MAIL_SERVER'],
            toaddrs=['ADMINS'], subject='Microblog Failure',
            credentials=auth, secure=secure)
        mail_handler.setLevel()
        (mail_handler)

The above programs only enable the mail logger function when debug mode is on, i.e. when True and the mail service is available.
Setting up the mail logger is a bit tedious, as one has to deal with many of the security optionals of the mail service. However, the code above actually creates the SMTPHandler instance. Set its level so that it only reports error messages.

There are two ways to test this feature. The simple such is to use the SMTP debugging service in Python to receive mail instead of sending it and print it to the large disk console. Open another terminal session and run the following command:

(venv) $ python -m smtpd -n -c DebuggingServer localhost:8025

Set MAIL_SERVER=localhost and MAIL_PORT=8025. If you are using Linux or Mac OS, prefix the command with sudo. If you are using Windows, open the terminal with administrator privileges. This command requires administrator privileges. Ports below 1024 are administrator-only ports. Alternatively, you can change to a higher port number, such as 5025, and set the MAIL_PORT variable to the port selected in your environment so that administrator privileges are not required.

The SMTP debugging service in the second terminal is kept running by setting export MAIL_SERVER=localhost and MAIL_PORT=8025 (set in Windows) in the first terminal environment. Since the application does not send mail in debug mode, set the FLASK_DEBUG variable to 0 or nothing. Run the application, trigger the SQLAlchemy error again, and you'll see the email with the stack error in the terminal.

The second way to test this is to configure a real mail service. Here's how to use Gmail's mail service:

export MAIL_SERVER=
export MAIL_PORT=587
export MAIL_USE_TLS=1
export MAIL_USERNAME=<your-gmail-username>
export MAIL_PASSWORD=<your-gmail-password>

On Windows systems, use set instead of export.

6. Recording to documents

app/__init__.py

# ...
from  import RotatingFileHandler
import os
 
# ...
 
if not :
    # ...
 
    if not ('logs'):
        ('logs')
    file_handler = RotatingFileHandler('logs/', maxBytes=10240,
                                       backupCount=10)
    file_handler.setFormatter((
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
    file_handler.setLevel()
    (file_handler)
 
    ()
    ('Microblog startup')

I wrote the log file with the filename to the logs folder.

The RotatingFileHandler class ensures that the log file is not too large. In this example I have limited the size of the log file to 10KB and kept the last 10 log files as backups.

class provides a customized format for log messages. Since these messages are logged to a file, the format contains a timestamp, log level, message, source file where the log entry starts, and line number.

I set the logging level to INFO for both the application logger and the file logger processor. (The logging levels are, in order of severity: DEBUG, INFO, WARNING, ERROR, CRITICAL).

Each time the server starts it writes lines to the log, and when this application is running in the production server, you can see when the server is restarted above.

7. Fixing username renaming vulnerability

RegistrationForm already implements user name validation, but the edit form does not.

app/

class EditProfileForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
    submit = SubmitField('Submit')
 
    def __init__(self, original_username, *args, **kwargs):
        super(EditProfileForm, self).__init__(*args, **kwargs)
        self.original_username = original_username
 
    def validate_username(self, username):
        if  != self.original_username:
            user = .filter_by(username=).first()
            if user is not None:
                raise ValidationError('Please use a different username.')

This can be accomplished with a custom validation method, but here's another overloaded constructor that accepts the original username as a parameter. This username is saved as an instance variable and then checked with the validate_username() method. If the username in the form is the same as the original username, there is no need to check the database ring for duplicate entries.

Add the original username as an argument to the view function.

app/

@('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm(current_user.username)
    # ...

summarize

The above is a personal experience, I hope it can give you a reference, and I hope you can support me more.