2018-01-16T20:22:10Z

The Flask Mega-Tutorial Part VII: Error Handling

This is the seventh installment of the Flask Mega-Tutorial series, in which I'm going to tell you how to do error handling in a Flask application.

For your reference, below is a list of the articles in this series.

In this chapter I'm taking a break from coding new features into my microblog application, and instead will discuss a few strategies to deal with bugs, which invariably make an appearance in every software project. To help illustrate this topic, I intentionally let a bug slip in the code that I've added in Chapter 6. Before you continue reading, see if you can find it!

The GitHub links for this chapter are: Browse, Zip, Diff.

Error Handling in Flask

What happens when an error occurs in a Flask application? The best way to find out is to experience it first hand. Go ahead and start the application, and make sure you have at least two users registered. Log in as one of the users, open the profile page and click the "Edit" link. In the profile editor, try to change the username to the username of another user that is already registered, and boom! This is going to bring a scary looking "Internal Server Error" page:

Internal Server Error

If you look in the terminal session where the application is running, you will see a stack trace of the error. Stack traces are extremely useful in debugging errors, because they show the sequence of calls in that stack, all the way 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)
[2021-06-14 22:40:02,027] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
  File "venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", in _execute_context
    context)
  File "venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", in do_execute
    cursor.execute(statement, parameters)
sqlite3.IntegrityError: UNIQUE constraint failed: user.username

The stack trace indicates what is the bug. The application allows a user to change the username, and does not validate that the new username chosen does not collide with another user already in the system. The error comes from SQLAlchemy, which tries to write the new username to the database, but the database rejects it because the username column is defined with unique=True.

It is important to note that the error page that is presented to the user does not provide much information about the error, and that is good. I definitely do not want users to learn that the crash was caused by a database error, or what database I'm using, or what are some of the table and field names in my database. All that information should be kept internal.

There are a few things that are far from ideal. I have an error page that is very ugly and does not match the application layout. I also have important application stack traces being dumped on a terminal that I need to constantly watch to make sure I don't miss any errors. And of course I have a bug to fix. I'm going to address all these issues, but first, let's talk about Flask's debug mode.

Debug Mode

The way you saw that errors are handled above is great for a system that is running on a production server. If there is an error, the user gets a vague error page (though I'm going to make this error page nicer), and the important details of the error are in the server process output or in a log file.

But when you are developing your application, you can enable debug mode, a mode in which Flask outputs a really nice debugger directly on your browser. To activate debug mode, stop the application, and then set the following environment variable:

(venv) $ export FLASK_ENV=development

If you are on Microsoft Windows, remember to use set instead of export.

After you set FLASK_ENV, restart the server. The output on your terminal is going to be slightly different than what you are used to see:

(venv) microblog2 $ flask run
 * Serving Flask app 'microblog.py' (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 118-204-854

Now make the application crash one more time to see the interactive debugger in your browser:

Flask Debugger

The debugger allows you expand each stack frame and see the corresponding source code. You can also open a Python prompt on any of the frames and execute any valid Python expressions, for example to check the values of variables.

It is extremely important that you never run a Flask application in debug mode on a production server. The debugger allows the user to remotely execute code in the server, so it can be an unexpected gift to a malicious user who wants to infiltrate your application or your server. As an additional security measure, the debugger running in the browser starts locked, and on first use will ask for a PIN number, which you can see in the output of the flask run command.

Since I am in the topic of debug mode, I should mention the second important feature that is enabled with debug mode, which is the reloader. This is a very useful development feature that automatically restarts the application when a source file is modified. If you run flask run while in debug mode, you can then work on your application and any time you save a file, the application will restart to pick up the new code.

Custom Error Pages

Flask provides a mechanism for an application to install its own error pages, so that your users don't have to see the plain and boring default ones. As an example, let's define custom error pages for the HTTP errors 404 and 500, the two most common ones. Defining pages for other errors works in the same way.

To declare a custom error handler, the @errorhandler decorator is used. I'm going to put my error handlers in a new app/errors.py module.

app/errors.py: Custom error handlers

from flask import render_template
from app import app, db

@app.errorhandler(404)
def not_found_error(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return render_template('500.html'), 500

The error functions work very similarly to view functions. For these two errors, I'm returning the contents of their respective templates. Note that both functions return a second value after the template, which is the error code number. For all the view functions that I created so far, I did not need to add a second return value because the default of 200 (the status code for a successful response) is what I wanted. In this case these are error pages, so I want the status code of the response to reflect that.

The error handler for the 500 errors could be invoked after a database error, which was actually the case with the username duplicate above. To make sure any failed database sessions do not interfere with any database accesses triggered by the template, I issue a session rollback. This resets the session to a clean state.

Here is the template for the 404 error:

app/templates/404.html: Not found error template

{% extends "base.html" %}

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

And here is the one for the 500 error:

app/templates/500.html: Internal server error template

{% extends "base.html" %}

{% 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') }}">Back</a></p>
{% endblock %}

Both templates inherit from the base.html template, so that the error page has the same look and feel as the normal pages of the application.

To get these error handlers registered with Flask, I need to import the new app/errors.py module after the application instance is created:

app/__init__.py: Import error handlers

# ...

from app import routes, models, errors

If you set FLASK_ENV=production in your terminal session and then trigger the duplicate username bug one more time, you are going to see a slightly more friendly error page.

Custom 500 Error Page

Sending Errors by Email

The other problem with the default error handling provided by Flask is that there are no notifications, stack trace for errors are printed to the terminal, which means that the output of the server process needs to be monitored to discover errors. When you are running the application during development, this is perfectly fine, but once the application is deployed on a production server, nobody is going to be looking at the output, so a more robust solution needs to be put in place.

I think it is very important that I take a proactive approach regarding errors. If an error occurs on the production version of the application, I want to know right away. So my first solution is going to be to configure Flask to send me an email immediately after an error, with the stack trace of the error in the email body.

The first step is to add the email server details to the configuration file:

config.py: Email configuration

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

The configuration variables for email include the server and port, a boolean flag to enable encrypted connections, and optional username and password. The five configuration variables are sourced from their environment variable counterparts. If the email server is not set in the environment, then I will use that as a sign that emailing errors needs to be disabled. The email server port can also be given in an environment variable, but if not set, the standard port 25 is used. Email server credentials are by default not used, but can be provided if needed. The ADMINS configuration variable is a list of the email addresses that will receive error reports, so your own email address should be in that list.

Flask uses Python's logging package to write its logs, and this package already has the ability to send logs by email. All I need to do to get emails sent out on errors is to add a SMTPHandler instance to the Flask logger object, which is app.logger:

app/__init__.py: Log errors by email

import logging
from logging.handlers import SMTPHandler

# ...

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

As you can see, I'm only going to enable the email logger when the application is running without debug mode, which is indicated by app.debug being True, and also when the email server exists in the configuration.

Setting up the email logger is somewhat tedious due to having to handle optional security options that are present in many email servers. But in essence, the code above creates a SMTPHandler instance, sets its level so that it only reports errors and not warnings, informational or debugging messages, and finally attaches it to the app.logger object from Flask.

There are two approaches to test this feature. The easiest one is to use the SMTP debugging server from Python. This is a fake email server that accepts emails, but instead of sending them, it prints them to the console. To run this server, open a second terminal session and run the following command on it:

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

Leave the debugging SMTP server running and go back to your first terminal and set export MAIL_SERVER=localhost and MAIL_PORT=8025 in the environment (use set instead of export if you are using Microsoft Windows). Make sure the FLASK_ENV variable is set to production or not set at all, since the application will not send emails in debug mode. Run the application and trigger the SQLAlchemy error one more time to see how the terminal session running the fake email server shows an email with the full stack trace of the error.

A second testing approach for this feature is to configure a real email server. Below is the configuration to use your Gmail account's email server:

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

If you are using Microsoft Windows, remember to use set instead of export in each of the statements above.

The security features in your Gmail account may prevent the application from sending emails through it unless you explicitly allow "less secure apps" access to your Gmail account. You can read about this here, and if you are concerned about the security of your account, you can create a secondary account that you configure just for testing emails, or you can enable less secure apps only temporarily to run this test and then revert back to the default.

Yet another alternative is to use a dedicated email service such as SendGrid, which allows you to send up to 100 emails per day on a free account. The SendGrid blog has a detailed tutorial on using the service in a Flask application.

Logging to a File

Receiving errors via email is nice, but sometimes this isn't enough. There are some failure conditions that do not end in a Python exception and are not a major problem, but they may still be interesting enough to save for debugging purposes. For this reason, I'm also going to maintain a log file for the application.

To enable a file based log another handler, this time of type RotatingFileHandler, needs to be attached to the application logger, in a similar way to the email handler.

app/__init__.py: Logging to a file

# ...
from logging.handlers import RotatingFileHandler
import os

# ...

if not app.debug:
    # ...

    if not os.path.exists('logs'):
        os.mkdir('logs')
    file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240,
                                       backupCount=10)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)

    app.logger.setLevel(logging.INFO)
    app.logger.info('Microblog startup')

I'm writing the log file with name microblog.log in a logs directory, which I create if it doesn't already exist.

The RotatingFileHandler class is nice because it rotates the logs, ensuring that the log files do not grow too large when the application runs for a long time. In this case I'm limiting the size of the log file to 10KB, and I'm keeping the last ten log files as backup.

The logging.Formatter class provides custom formatting for the log messages. Since these messages are going to a file, I want them to have as much information as possible. So I'm using a format that includes the timestamp, the logging level, the message and the source file and line number from where the log entry originated.

To make the logging more useful, I'm also lowering the logging level to the INFO category, both in the application logger and the file logger handler. In case you are not familiar with the logging categories, they are DEBUG, INFO, WARNING, ERROR and CRITICAL in increasing order of severity.

As a first interesting use of the log file, the server writes a line to the logs each time it starts. When this application runs on a production server, these log entries will tell you when the server was restarted.

Fixing the Duplicate Username Bug

I have exploited the username duplication bug for too long. Now that I have showed you how to prepare the application to handle this type of errors, I can go ahead and fix it.

If you recall, the RegistrationForm already implements validation for usernames, but the requirements of the edit form are slightly different. During registration, I need to make sure the username entered in the form does not exist in the database. On the edit profile form I have to do the same check, but with one exception. If the user leaves the original username untouched, then the validation should allow it, since that username is already assigned to that user. Below you can see how I implemented the username validation for this form:

app/forms.py: Validate username in edit profile form.

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 username.data != self.original_username:
            user = User.query.filter_by(username=self.username.data).first()
            if user is not None:
                raise ValidationError('Please use a different username.')

The implementation is in a custom validation method, but there is an overloaded constructor that accepts the original username as an argument. This username is saved as an instance variable, and checked in the validate_username() method. If the username entered in the form is the same as the original username, then there is no reason to check the database for duplicates.

To use this new validation method, I need to add the original username argument in the view function, where the form object is created:

app/routes.py: Validate username in edit profile form.

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

Now the bug is fixed and duplicates in the edit profile form will be prevented in most cases. This is not a perfect solution, because it may not work when two or more processes are accessing the database at the same time. In that situation, a race condition could cause the validation to pass, but a moment later when the rename is attempted the database was already changed by another process and cannot rename the user. This is somewhat unlikely except for very busy applications that have a lot of server processes, so I'm not going to worry about it for now.

At this point you can try to reproduce the error one more time to see how the new form validation method prevents it.

275 comments

  • #176 Joshua Muwanguzi said 2020-04-20T07:25:46Z

    Hey Miguel thanks so much for the work you put in. I really would like to understant what these errors here mean. I am just a beginner and this is the first course I have seriously taken on any web development framework and its really helping me. I would like to know, even after using smtp handler, is the error still supposed to show in the local enviroment as it is, or it just gets sent to the email.

    I get the following errors when I run the server with just the STMPHandler and the stmpd debugging server does not display anything.

    $ flask run * Serving Flask app "microblog.py" * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: off * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 127.0.0.1 - - [20/Apr/2020 10:04:59] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [20/Apr/2020 10:04:59] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [20/Apr/2020 10:04:59] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [20/Apr/2020 10:04:59] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [20/Apr/2020 10:05:01] "GET /user/jb HTTP/1.1" 200 - 127.0.0.1 - - [20/Apr/2020 10:05:01] "GET /user/jb HTTP/1.1" 200 - 127.0.0.1 - - [20/Apr/2020 10:05:03] "GET /edit_profile HTTP/1.1" 200 - 127.0.0.1 - - [20/Apr/2020 10:05:03] "GET /edit_profile HTTP/1.1" 200 - [2020-04-20 10:05:10,119] ERROR in app: Exception on /edit_profile [POST] Traceback (most recent call last): File "c:\users\dell e7240\desktop\microblog\env\lib\site-packages\sqlalchemy\engine\base.py", line 1247, in _execute_context self.dialect.do_execute( File "c:\users\dell e7240\desktop\microblog\env\lib\site-packages\sqlalchemy\engine\default.py", line 590, in do_execute cursor.execute(statement, parameters) sqlite3.IntegrityError: UNIQUE constraint failed: user.username

  • #177 Miguel Grinberg said 2020-04-20T10:35:03Z

    @Joshua: the error is "unique constraint failed". It means you are trying to add a username that already exists in your database.

  • #178 alex said 2020-04-25T13:38:17Z

    Hello!

    I fixed the dublication of a username bug just with this function inside my EditProfileForm class:

    def validate_username(self, username): u = User.query.filter_by(username = username.data).first() if u is not None and u != current_user:

    raise ValidationError('Select differnet username dog')

    and i imported current_user module to my forms.py file, can it be the solution too for this problem or it has a flaws?(sorry for english)

  • #179 Miguel Grinberg said 2020-04-25T21:59:13Z

    @alex: this article shows a different way of addressing this problem, see the Fixing the Duplicate Username Bug. Your solution has the disadvantage that you have a dependency between Flask-Login and Flask-WTF, since you are importing and using current_user in your form class. In general it is preferable to keep inter-dependencies at a minimum. My solution does not have this problem.

  • #180 Hyeong said 2020-04-26T05:37:19Z

    Hello Miguel,

    I am trying to run the DebuggingServer command on my second terminal, but when I type in

    python -m smtpd -n -c DebugginServer localhost:8025

    then the terminal would not proceed as if it's expecting something more. I'm in MacOS. Have you seen such behavior? How should I resolve it?

    Much Thanks,

  • #181 Miguel Grinberg said 2020-04-26T14:44:56Z

    @Hyeong: what do you mean by "not proceed"? The debugging server will wait until an email is sent, you are not going to see any output right after starting this debugging server.

  • #182 Joshua said 2020-05-08T05:49:28Z

    Can I configure the email server in the .flaskenv file?

  • #183 Miguel Grinberg said 2020-05-08T22:51:08Z

    @Joshua: you can, but it is better to not mix Flask variables with others that can be of a sensitive nature. The recommended way to work with environment variables is to put Flask specific variables in .flaskenv, and other configuration variables in .env (dot env). You can commit .flaskenv to source control, but .env must be put in the .gitignore file to prevent accidental commit of your secrets.

  • #184 Shubham Jha said 2020-05-10T10:40:12Z

    I really liked this tutorial.

  • #185 Demola Adebowale said 2020-05-11T04:00:10Z

    hi Miguel, Thanks for a wonderful tutorial! I keep having this error after downloading and compare GITHUB code, I can't seems to figure anything with the code. as a newbie in python, Kindly help Error:

    flask.cli.NoAppException flask.cli.NoAppException: While importing "microblog", an ImportError was raised: Traceback (most recent call last): File "c:\server\www\microblog\venv\lib\site-packages\flask\cli.py", line 240, in locate_app __import__(module_name) File "C:\Server\www\microblog\microblog.py", line 1, in <module> from app import app, db File "C:\Server\www\microblog\app\__init__.py", line 45, in <module> from app import routes, models, errors ImportError: cannot import name 'errors' from partially initialized module 'app' (most likely due to a circular import) (C:\Server\www\microblog\app\__init__.py)
  • #186 Miguel Grinberg said 2020-05-11T10:30:48Z

    @Demola: do you have the errors.py file in the app directory?

  • #187 Mark said 2020-05-31T17:54:50Z

    Hi Miguel,

    Another big fan of your work here; I've learned so much from the tutorial so far. It's so well written, and I'm thoroughly enjoying it.

    Only problem I've found is one related to using my own SMTP server. I have a server via the webspace I use. I created a specific email address to use, and then added it to the MacOS Mail app to check it worked, and could successfully send and receive email, authenticate and so on.

    I then put the same details into my .flaskenv file (appreciate it's insecure; just did it without adding to VCS to help me debug!), and I get the following block of errors:

    Traceback (most recent call last): File "/Users/markwharry/.pyenv/versions/3.8.1/lib/python3.8/smtplib.py", line 391, in getreply line = self.file.readline(_MAXLINE + 1) File "/Users/markwharry/.pyenv/versions/3.8.1/lib/python3.8/socket.py", line 669, in readinto return self._sock.recv_into(b) socket.timeout: timed out

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last): File "/Users/markwharry/.pyenv/versions/3.8.1/lib/python3.8/logging/handlers.py", line 1008, in emit smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout) File "/Users/markwharry/.pyenv/versions/3.8.1/lib/python3.8/smtplib.py", line 253, in init (code, msg) = self.connect(host, port) File "/Users/markwharry/.pyenv/versions/3.8.1/lib/python3.8/smtplib.py", line 341, in connect (code, msg) = self.getreply() File "/Users/markwharry/.pyenv/versions/3.8.1/lib/python3.8/smtplib.py", line 394, in getreply raise SMTPServerDisconnected("Connection unexpectedly closed: " smtplib.SMTPServerDisconnected: Connection unexpectedly closed: timed out

    Appreciate you can't help me troubleshoot my own server, but the exact same details work fine on my Mail app, they just don't seem to play nicely with SMTPLib. Do you have any advice?

  • #188 Miguel Grinberg said 2020-06-01T09:06:48Z

    @Mark: if you have access to this SMTP server then check its logs to see if the connection requests from your Flask app were received, and if they were, why they weren't answered. I suspect something in your configuration is incorrect.

  • #189 Mark said 2020-06-15T12:59:00Z

    Hi Miguel,

    Thanks for such an awesome series of posts.

    I had a few questions.

    1 - When trying the 2nd testing approach for error logs that are emailed, do you enter the example configuration details in the same terminal or a second terminal, such as in the 1st testing approach? If it's supposed to be entered into the same terminal, how does one properly initiate the flask app? My issue is that as once the email configuration details are entered, a "session" begins (ie, the terminal lines appears as "> " instead of "(venv) ... ") and a new command cannot be entered into the terminal until I "end" the session (ie, by entering ctrl+c). 2 - Related to my confusion in #1... I decided to replace the SMTPHandler's arguments with the server/login details so I wouldn't have to worry about making a mistake in #1, the file executes with showing any errors and when replicating the SQLAlchemy error, and email is not triggered by my Gmail. I decided to copy-and-paste code from Stack Overflow (pasted below) in place of your code outlined here, to see if my issue pertained to my entering of the server/login details, but when using this code, an email was triggered properly. So, I'm not sure what the issue is. Here's the code from Stack Overflow that I am referring to:

    import logging import logging.handlers

    smtp_handler = logging.handlers.SMTPHandler(mailhost=("smtp.example.com", "port #"), fromaddr="from@example.com", toaddrs="to@example.com", subject=u"AppName error!" credentials=("your@email.com", "yourpassword") secure=() )

    logger = logging.getLogger() logger.addHandler(smtp_handler)

    try: break except Exception as e: logger.exception('Unhandled Exception')

  • #190 Miguel Grinberg said 2020-06-15T15:33:18Z

    @Mark: are you using a bash type shell, or are you on Windows? If you use bash, then you can set a variable with the export keyword, and the setting should remain for as long as you keep that terminal window open. If you are on windows, use set instead of export on your command prompt session. The settings for your email server need to be available in the terminal where you are running the Flask application.

    My guess is that the code you pasted from StackOverflow works because it asks you to type your credentials directly in the code, which is a really bad practice. Once you fix your environment variables my code should work as well.

  • #191 Mark said 2020-06-15T17:41:23Z

    @Miguel Grinberg

    Thanks for answering so quickly, and my apologies for the poorly formatted question.

    I am using bash on Linux.

    It was my inkling that it had to do with the environment variables when I saw the StackOverflow code work, too.

    So, I guess this is more of a bash question more than anything...

    After I type in the export command for the 5 email server variables, as described in this tutorial, I'm no longer able to type in any commands on bash.

    The key thing that changes is the command line.

    It goes from "(venv) marks_computer: ~/some_directory/microblog$" to simply "> ". If I type anything and hit enter, including "flask run", nothing happens. The only thing I can do is "end the session" for lack of a better term by hitting ctrl + c.

    I hope the way I'm describing this scenario makes sense.

    Is there something I'm doing incorrectly here?

    Thanks.

  • #192 Miguel Grinberg said 2020-06-17T10:53:33Z

    @Mark: the command that you are typing to set your variable must be incomplete. So when you press Enter, bash shows the alternate ">" prompt to allow you to continue entering your command. For example, I get this if I forget to close quotes:

    (venv) $ export FOO="bar >

    But it works if I close them:

    (venv) $ export FOO="bar" (venv) $

    Maybe some character that you are entering requires escaping, like for example, if your password had a quote character and you don't escape it, bash will think you are starting a string, like I just did above.

  • #193 Mark said 2020-06-17T12:40:01Z

    @Miguel: That was my issue, thank you for helping me make sense of that.

    I'm still unable to trigger the email, but it's quite possible that I made a mistake somewhere that I'm overlooking.

    Thanks for the help!

  • #194 Filipe Bezerra said 2020-06-23T13:43:38Z

    For me, the second testing approach I used my real Gmail account using App Passwords, so if anyone want to use here's the procedures: - Set up an App Password (https://support.google.com/accounts/answer/185833) - Create a .env file (or use .flaskenv file already created) in the root of the project - Put the configuration in it

    MAIL_SERVER=smtp.googlemail.com MAIL_PORT=587 MAIL_USE_TLS=1 MAIL_USERNAME= MAIL_PASSWORD=

    And now you'll be notified by your Gmail.

  • #195 Nathan Faulkner said 2020-06-30T13:08:05Z

    Hi, I am getting tons from this blog! One question regarding this bit of code: " def validate_username(self, username): if username.data != self.original_username: user = User.query.filter_by(username=self.username.data).first() " ... I think I would learn something to know why both "username.data" and "self.username.data" are used here. I figure that passing the variable "username" to this function is just a convention (?) whereas the way the validator actually gets associated with the username field is by parsing the string of the name of the validator function. Is this right? So, for instance, looks like it could just be " def validate_username(self, field): if field.data != self.original_username: user = User.query.filter_by(username=field.data).first() " ... and it would still work? (Seemed to work for me.) But why does "self.username.data" also work? Is it because the validator only checks the data after the form has been submitted and thus self.username.data is the same as username.data (or even field.data)? ... That's what I decided after experimenting. Do I have it right? (I'm having trouble with a custom validator on another project, so I am really studying this one that actually worked for

    Like I said, I think may learn something here.

    Thanks!

  • #196 Miguel Grinberg said 2020-06-30T21:55:32Z

    @Nathan: the username field passed as an argument is actually the same field that the object has in it as self.username. I didn't realize when I coded this but I used the two ways to reference this field, while I should have always used the passed argument. It works either way because it is always the same field object.

  • #197 Avner said 2020-07-08T02:07:01Z

    Hi Miguel, Thanks for the thorough tutorial.

    I have a question regarding sending emails.

    In my case, the web application is in a docker container. (I also have containers for nginx, webapp, postgres, celery, and redis ) I still want to use an smtp server on my localhost. What would be the setting to reach out from the docker container, to the smtp server on the host?

    I read (here) https://stackoverflow.com/questions/31324981/how-to-access-host-port-from-docker-container that I can use the IP address of the docker0 interface, so I set the MAIL_SERVER to this address.

    I caused an error (division by 0) and I can see the stack trace when trailing the log of the web app container "docker logs -f webserver_web_1" but the smtp server does not show anything.

    (the smtp server is working. I verified that with the microblog code, it does show error emails)

    The configuration for my web app container is:

    services: web: restart: always ports: - "8000:8000" expose: - "8000" volumes: - data2:/usr/src/app/web/app/avner/img depends_on: - postgres environment: - PYTHONUNBUFFERED=1 - FLASK_APP=run.py - FLASK_DEBUG=0 - POSTGRES_URL2=postgres:5432 command: flask run --host=0.0.0.0 --port 8000 networks: testing_net: ipv4_address: 172.28.1.1

    The email related variables are: app.config[MAIL_SERVER] 172.28.0.1

    I also tried setting the MAIL_SERVER to: 172.17.0.1

    app.config[MAIL_PORT] 8025 app.config[MAIL_USERNAME] None app.config[MAIL_PASSWORD] None app.config[MAIL_USE_TLS] False app.config[ADMINS] ['foo@gmail.com']

    The docker related IP addresses are:

    ifconfig | grep -A 2 docker0 docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255

    ip route show default via 172.28.0.1 dev eth0 172.28.0.0/16 dev eth0 proto kernel scope link src 172.28.1.1

    extract the IP address:

    hostip=$(ip route show | awk '/default/ {print $3}') echo $hostip 172.28.0.1

  • #198 Miguel Grinberg said 2020-07-08T11:55:56Z

    @Avner: I always use the host.docker.internal hostname to reference the docker host from inside a container. Give that a try.

  • #199 Avner said 2020-07-10T00:58:22Z

    I cannot ping host.docker.internal from within the container docker exec -it webserver_web_1 bash flask@31fb0b0d4fdc:/usr/src/app/web$ ping host.docker.internal ping: host.docker.internal: Name or service not known

    I also didn't succeed to run the smtpd server on localhost and communicate to it from the docker container. (building the docker container in "network_mode: host" broke other network stuff)

    However, if I start the smtpd server from a terminal (tty1) using the docker host IP address python -m smtpd -n -c DebuggingServer 172.17.0.1:8025 and I send an email from a terminal (tty2) inside the docker container python smtpd_senddata.py then I can see that the email arrives in the smtpd server!

    Thanks

    smtpd_senddata.py

    import smtplib import email.utils from email.mime.text import MIMEText

    # Create the message msg = MIMEText('This is the body of the message.') msg['To'] = email.utils.formataddr(('Recipient', 'recipient@example.com')) msg['From'] = email.utils.formataddr(('Author', 'author@example.com')) msg['Subject'] = 'Simple test message'

    server = smtplib.SMTP('172.17.0.1', 8025) server.set_debuglevel(True) # show communication with the server try: server.sendmail('author@example.com', ['recipient@example.com'], msg.as_string()) finally: server.quit()

  • #200 Avner said 2020-07-10T01:07:35Z

    p.s.

    I just discovered that I can also run the smtpd server on my localhost using the IP addresses of the wired, and wireless network interfaces python -m smtpd -n -c DebuggingServer 192.168.1.74:8025 python -m smtpd -n -c DebuggingServer 192.168.1.78:8025 and I send an email from a terminal (tty2) inside the docker container to these addresses and see that the email arrives in the smtpd server (but I cannot use the loopback IP address, i.e. "localhost", or "127.0.0.1" )

    ifconfig ... docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255 ...

    wired network interface

    enp0s31f6: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.1.78 netmask 255.255.255.0 broadcast 192.168.1.255 ...

    wireless network interface

    wlp4s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.1.74 netmask 255.255.255.0 broadcast 192.168.1.255 ...

Leave a Comment