Nhấn vào theo dõi chúng tôi để cập nhật các bài viết mới nhất. Theo dõi!

Hướng dẫn lập trình Flask – Phần 7: Xử lý lỗi

Trong phần này, chúng ta sẽ tìm hiểu cách xử lý lỗi trong các ứng dụng Flask.
Bài viết liên quan:
Chúng ta sẽ tạm ngừng việc thêm chức năng mới vào ứng dụng nhỏ của chúng ta. Thay vào đó, chúng ta sẽ tìm hiểu về cách đối phó với các lỗi – điều luôn luôn xảy ra trong bất kỳ dự án phần mềm nào. Để minh họa cho phần này, chúng ta đã cố ý để một lỗi trong mã nguồn được thêm vào trong Phần 6. Bạn có thể thử tìm xem nó là gì nếu bạn muốn.

Xử lý lỗi trong Flask

Điều gì sẽ xảy ra nếu một ứng dụng Flask gặp lỗi? Cách hay nhất để trả lời câu hỏi này là trải nghiệm nó. Bạn hãy chạy ứng dụng và đăng ký ít nhất hai user. Sau đó bạn đăng nhập bằng user thứ nhất, vào trong trang hồ sơ cá nhân và bấm vào liên kết “Edit” để vào trình soạn thảo hồ sơ cá nhân. Trong form soạn thảo, bạn hãy thử đổi username thành username của user thứ hai và bấm “Submit”. Bạn sẽ thấy một trang thông báo lỗi “Internal Server Error” như sau:
Nếu bạn kiểm tra cửa sổ lệnh dùng để chạy chương trình, bạn sẽ thấy các thông điệp dò lỗi (stack trace). Stack trace rất hữu ích cho việc tìm lỗi vì nó chỉ ra thứ tự thực hiện của các hàm cho đến khi xảy ra lỗi:
(myenv) $ flask run
 * Serving Flask app "myblog.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 - - [08/Sep/2019 16:55:34] "GET /edit_profile HTTP/1.1" 200 -
[2019-09-08 16:55:40,712] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
  File "/home/thaipt/Works/Flask/myblog/myenv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1249, in _execute_context
    cursor, statement, parameters, context
  File "/home/thaipt/Works/Flask/myblog/myenv/lib/python3.7/site-packages/sqlalchemy/engine/default.py", line 552, in do_execute
    cursor.execute(statement, parameters)
sqlite3.IntegrityError: UNIQUE constraint failed: user.username
Dòng cuối trong stack trace cho chúng ta biết ứng dụng đã gặp lỗi gì. Ứng dụng cho phép user thay đổi username nhưng không kiểm tra xem username mới được nhập vào có trùng lặp với bất kỳ username nào hiện đang được lưu trong cơ sở dữ liệu không. SQLAlchemy báo lỗi này khi lưu username mới vào cơ sở dữ liệu nhưng không được vì cột username được định nghĩa là duy nhất trong cơ sở dữ liệu (unique = True).

Một điểm quan trọng cần lưu ý ở đây là trên trang báo lỗi mà user nhìn thấy không có các thông tin cụ thể về lỗi, và đây là điều cần thiết bởi vì chúng ta không muốn user biết rõ là lỗi do cơ sở dữ liệu cũng như chúng ta đang dùng cơ sở dữ liệu gì, hoặc là tên của các bản và các trường trong cơ sở dữ liệu. Các thông tin này chỉ nên được lưu hành nội bộ cho những người liên quan (phân tích/thiết kế hệ thống, lập trình viên …).

Tuy vậy, chúng ta vẫn cần thay đổi một số chi tiết trong trang này vì cách trình bày không đẹp và không ăn nhập với giao diện chung của ứng dụng. Chúng ta cũng phải liên tục theo dõi các thông báo lỗi từ stacktrace trong cửa sổ lệnh. Và cuối cùng, chúng ta cũng phải phải sửa lỗi. Nhưng việc đầu tiên chúng ta cần làm là vào chế độ dò tìm lỗi (debug mode) trong Flask.

Chế độ dò lỗi (Debug Mode)

Cách xử lý lỗi như trên là trường hợp điển hình cho các hệ thống trong môi trường production. Nếu ứng dụng có lỗi, user sẽ nhận được một trang thông báo lỗi tổng quát (chút nữa chúng ta sẽ “trang điểm” cho trang này đẹp hơn một chút), còn các chi tiết quan trọng về lỗi sẽ được đưa ra ở phía server hoặc lưu lại trong các file nhật ký (log) của ứng dụng.

Tuy nhiên, khi đang trong tiến trình phát triển ứng dụng, chúng ta có thể chuyển sang chế độ dò lỗi. Trong chế độ này, Flask sẽ trình bày các chi tiết liên quan đến lỗi trên trình duyệt. Để kích hoạt chế độ dò lỗi, bạn hãy ngừng ứng dụng và thiết lập biến môi trường như sau:
(myenv) $ export FLASK_DEBUG=1
Nếu bạn đang sử dụng Microsoft Windows, hãy dùng lệnh set thay vì export.

Sau khi biến môi trường FLASK_DEBUG được thiết lập, hãy khởi động lại ứng dụng. Lần này bạn sẽ thấy các dòng trạng thái khi chạy ứng dụng hơi khác so với trước đây:
(myenv) $ flask run
 * Serving Flask app "myblog.py" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 294-550-837
Bây giờ hãy lặp lại lỗi ở trên, lần này bạn sẽ thấy các thông báo lỗi trong một chương trình dò lỗi tương tác (interactive debugger) trong trình duyệt:
Trình dò lỗi này cho phép chúng ta mở rộng các khung có chứa hàm được gọi và thấy mã nguồn được sử dụng. Chúng ta cũng có thể mở một dấu nhắc lệnh Python trên mỗi khung và thực hiện các biểu thức Python hợp lệ, ví dụ như kiểm tra giá trị của các biến.

Nên nhớ là bạn không nên chạy các ứng dụng Flask trong chế độ dò lỗi trong các môi trường production. Trình dò lỗi sẽ cho phép user thực hiện các đoạn mã trên server từ xa, và đây là một cơ hội bằng vàng cho những user ác ý muốn thâm nhập hệ thống của bạn. Để tăng cường khả năng bảo mật, trình dò lỗi sẽ bị khóa lúc khởi động và sẽ yêu cầu nhập số PIN khi bạn bắt đầu sử dụng (số PIN sẽ được in ra trong cửa sổ lệnh khi bạn chạy lệnh flask run)

Và vì chúng ta đang ở trong chủ đề dò lỗi, chúng ta sẽ nói về chức năng quan trọng thứ hai khi sử dụng chế độ dò lỗi, đó là chương trình để tải lại dữ liệu (reloader). Đây là một chức năng rất hữu ích trong quá trình phát triển ứng dụng. Nó sẽ tự động khởi động lại ứng dụng khi chúng ta thay đổi một file mã nguồn. Nếu bạn chạy lệnh flask run trong chế độ dò lỗi, mỗi khi bạn sửa đổi một file mã nguồn, ứng dụng sẽ tự khởi động lại và sử dụng mã mới.

Tùy biến trang báo lỗi

Flask cho phép ứng dụng tùy biến và thay thế thông báo lỗi mặc định. Chúng ta có thể sử dụng lợi thế này trong ứng dụng của chúng ta và thử thay thế các trang báo lỗi tương đối phổ biến HTTP 404 và 500 bằng các thông báo lỗi của chúng ta. Chúng ta cũng có thể làm tương tự với các trang báo lỗi khác.

Để khai báo một trang báo lỗi tùy biến, chúng ta sẽ dùng decorator @errorhandler và đặt hàm xử lý lỗi trong module app/errors.py
  • app/errors.py: Hàm xử lý lỗi
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
Cách viết các hàm báo lỗi cũng tương tự như các hàm hiển thị. Chúng ta sẽ lần lượt trả về các nội dung tương ứng cho mỗi lỗi trong từng template riêng. Để ý rằng ngoài các template, các hàm này cũng trả về giá trị thứ hai là mã lỗi (error code). Khi chúng ta viết các hàm hiển thị, chúng ta không cần dùng đến giá trị thứ hai này vì nó được mặc định là 200 (mã lỗi tương ứng với trường hợp mã ứng dụng đã trả lời thành công với yêu cầu từ trình duyệt của người dùng) và chúng ta chỉ cần như vậy. Nhưng trong trường hợp có lỗi, chúng ta cần trả về các mã lỗi tương ứng.

Phần xử lý cho mã lỗi 500 có thể liên quan đến một vấn đề từ cơ sở dữ liệu – và đúng như vậy, lỗi này xảy ra do việc trùng tên của user mà chúng ta đã thảo luận ở trên. Để bảo đảm các tác vụ bị lỗi của cơ sở dữ liệu không ảnh hưởng đến các tác vụ khác trong template, chúng ta sẽ gọi hàm session.rollback(). Hàm này sẽ khởi động lại phiên làm việc của chúng ta và đưa các dữ liệu trở lại trạng thái ban đầu (clean).

Sau đây là template cho lỗi 404:
  • app/templates/404.html: template cho lỗi 404 – không tìm thấy URL
{% extends "base.html" %}
 
{% block content %}
    <h1>File Not Found</h1>
    <p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}
Và template cho lỗi 500:
  • app/templates/500.html: template cho lỗi 500 – lỗi nội bộ của server
{% 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 %}
Cả hai template này đều kế thừa từ template base.html. Nhờ đó, cách trình bày trong hai template này vẫn giống như các trang khác trong ứng dụng.

Để đăng ký hàm xử lý lỗi với Flask, chúng ta cần tham chiếu đến module vừa tạo ra ở trên là app/errors.py sau khi tạo ra thực thể ứng dụng:
  • app/__init__.py: Tham chiếu đến hàm xử lý lỗi
...
 
from app import routes, models, errors
Nếu bạn đã thiết lập biến môi trường FLASK_DEBUG = 0 trong cửa sổ lệnh và tạo ra lỗi trùng tên user lần nữa, bạn sẽ thấy một trang báo lỗi tương đối thân thiện hơn như sau:

Thông báo lỗi qua email

Một vấn đề nữa với hàm xử lý lỗi mặc định của Flask là nó không có chức năng thông báo (notification). Mỗi khi xảy ra lỗi, các stack trace sẽ được in trực tiếp ra cửa sổ lệnh đồng nghĩa với việc chúng ta phải theo dõi quá trình chạy của ứng dụng trên server để phát hiện ra lỗi. Khi bạn chạy ứng dụng trong chế độ phát triển (development), đây không phải là vấn đề lớn. Nhưng sau khi ứng dụng chính thức hoạt động và được cài đặt trong môi trường production, sẽ không ai có thể liên tục theo dõi quá trình chạy ứng dụng. Vì vậy, chúng ta cần một giải pháp tốt hơn.

Trong việc theo dõi và xử lý lỗi, cách tốt nhất là chúng ta luôn chủ động: nếu hệ thống phát sinh lỗi, chúng ta phải được thông báo ngay lập tức. Vì vậy, việc đầu tiên cần làm là thiết lập Flask để nó sẽ tự động gởi email cho chúng ta ngay khi lỗi phát sinh với các thông tin chi tiết về lỗi (stack trace) trong nội dung email.

Bước đầu tiên là khai báo chi tiết về server cho email trong file cấu hình:
  • config.py: cấu hình cho email
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']
Các tham số cấu hình cho email bao gồm địa chỉ server và cổng (port), sử dụng chế độ mã hóa và các tùy chọn cho username và mật mã. Năm tham số này được lấy từ các biến môi trường tương ứng. Nếu chúng ta không tìm được các biến môi trường này, điều đó có nghĩa là môi trường mà ứng dụng được cài đặt không có email server và chúng ta phải tắt khả năng này. Địa chỉ cổng cho email server có thể được khai báo trong một biến môi trường, nhưng nếu không tìm thấy, ứng dụng sẽ sử dụng cổng quy ước cho email là 25. Username và mật mã cho email server không được sử dụng theo mặc định, nhưng có thể được cung cấp nếu cần thiết. Tham số ADMINS là một danh sách các email sẽ nhận được thông báo lỗi, vì vậy bạn có thể để địa chỉ email cá nhân của mình vào đây.

Flask sử dụng gói logging của Python để lưu lại các sự kiện khi chạy ứng dụng vào các file log (nhật ký). Gói này được tích hợp khả năng để gởi các nhật ký bằng email. Để ứng dụng có thể gởi email báo lỗi, điều chúng ta cần làm là thêm thực thể SMTPHandler vào đối tượng chuyên về xử lý nhật ký của Flask tên là app.logger:
  • app/__init__.py: Gởi nhật ký bằng 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='Myblog Failure',
            credentials=auth, secure=secure)
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)
Theo như đoạn mã trên, chúng ta chỉ sử dụng chức năng gởi nhật ký qua email nếu ứng dụng không chạy trong chế độ dò lỗi (debug – khi biến app.debug trả về True), và đồng thời có các thiết lập về máy chủ email trong cấu hình ứng dụng.

Quá trình thiết lập chức năng gởi nhật ký bằng email rất mất công vì có liên quan đến nhiều tùy chọn bảo mật trong các máy chủ email. Nhưng cơ bản là, đoạn mã trên tạo một thực thể SMTPHandler, xác định ngưỡng để nó chỉ gởi email cho các lỗi (error) chứ không phải các cảnh cáo (warning), thông tin (information) hay thông điệp dò lỗi (debugging mesage). Và cuối cùng, chúng ta sẽ kết nối thực thể này và đối tượng app.logger từ Flask.

Có hai cách để kiểm tra chức năng này. Cách dễ nhất là dùng một thành phần hỗ trợ là SMTP debugging server có sẵn trong Python. Đây là một máy chủ email giả (fake), nó có thể nhận/gởi email, nhưng thay vì gởi emai thật sự, nó sẽ in ra dòng lệnh. Để chạy server này, bạn hãy mở cửa sổ lệnh thứ hai và nhập lệnh sau đây:
(myenv) $ python -m smtpd -n -c DebuggingServer localhost:8025
Hãy để nó chạy và trở về cửa sổ lệnh đầu tiên và thiết lập các biến môi trường export MAIL_SERVER=localhost và MAIL_PORT=8025 (dùng lệnh set thay cho export nếu bạn đang dùng Windows). Bạn cũng nên kiểm tra để chắc rằng biến môi trường FLASK_DEBUG có giá trị 0 hay không tồn tại bởi vì ứng dụng sẽ không gởi email trong chế độ dò lỗi. Sau đó chạy ứng dụng và tạo lỗi trùng username lần nữa và quan sát cửa sổ lệnh thứ hai – nơi bạn đang chạy server email giả – bạn sẽ thấy nó in ra một email với nội dung là các chi tiết trong stack trace về lỗi.

Cách thứ hai để kiểm tra chức năng này là dùng một email server thật. Tiếp theo, chúng ta sẽ tạo các thiết lập để sử dụng email server từ dịch vụ Gmail:
export MAIL_SERVER=smtp.googlemail.com
export MAIL_PORT=587
export MAIL_USE_TLS=1
export MAIL_USERNAME=<username-cho-tài-khoản-gmail-của-bạn>
export MAIL_PASSWORD=<password-cho-tài-khoản-gmail-của-bạn>
(Đừng quên dùng set thay cho export trong các câu lệnh ở trên nếu bạn đang sử dụng Windows).

Các chức năng bảo mật trong thiết lập Gmail của bạn có thể không cho phép ứng dụng gởi email nếu bạn không thay đổi và chọn thiết lập cho phép “less secure apps”. Bạn có thể đọc thêm chi tiết về thiết lập này ở đây. Nếu cẩn thận, bạn có thể tạo một tài khoản Gmail thứ hai và dùng nó để kiểm tra email hoặc tạm thời cho phép thiết lập “less secure apps” để kiểm tra ứng dụng.

Ghi nhật ký vào file

Nhận các thông báo lỗi qua email rất tiện lợi, nhưng đôi khi vẫn chưa đủ. Trong một số tình huống, các lỗi không nghiêm trọng và không tạo ra các exception (ngoại lệ) trong Python, nhưng đủ để chúng ta ghi nhận các thông tin về chúng để kiểm tra lại. Vì lý do đó, chúng ta cũng cần xây dựng một hệ thống các file nhật ký (log file) cho ứng dụng.

Để sử dụng nhật ký qua file, chúng ta cần dùng một thư viện khác là RotatingFileHandler và liên kết nó với đối tượng app.logger tương tự như với trường hợp của email.
  • app/__init__.py: Ghi nhật ký vào 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/myblog.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('Myblog startup')
Chúng ta sẽ ghi vào file nhật ký tên là myblog.log trong thư mục logs, nếu chưa có thư mục này thì chúng ta sẽ tạo ra nó.

Lớp RotatingFileHandler rất tiện dụng vì nó sẽ luân phiên ghi nhật ký vào các file khác nhau (rotate) để bảo đảm rằng kích thước của các file này không quá lớn khi ứng dụng đang chạy. Trong trường hợp này, chúng ta giới hạn kích thước các file nhật ký là 10KB và chúng ta giữ lại mười log file dự phòng.

Lớp logging.Formatter cho phép sử dụng các định dạng tùy chọn (custom formatting) trong các thông tin nhật ký. Vì các thông tin này được ghi ra file, chúng ta muốn ghi càng nhiều chi tiết càng tốt. Vì vậy, chúng ta sẽ dùng định dạng bao gồm thời gian, cấp độ báo động (logging level), thông báo lỗi và tên của file mã nguồn gây ra lỗi và số dòng của lệnh đã gây ra lỗi trong flle đó.

Đồng thời, để khai thác tối đa các thông tin từ log, chúng ta cũng sẽ đặt cấp độ nhật ký ở mức INFO (thông tin) trong cả nhật ký ứng dụng và trong chương trình xử lý log. Trong trường hợp bạn không hiểu rõ, có năm cấp độ log khác nhau, gồm có: DEBUG, INFO, WARNING, ERROR và CRITICAL với mức độ nghiêm trọng từ thấp lên cao.

Tác dụng đầu tiên của hệ thống log là các trạng thái khi ứng dụng khởi động sẽ được lưu vào đây. Trong môi trường production, các chi tiết này trong log file sẽ cho biết khi nào ứng dụng được khởi động lại.

Sửa lỗi trùng username

Giờ là lúc chúng ta sửa lỗi trùng username mà chúng ta cố tình chừa lại. Nếu bạn còn nhớ, fom đăng ký có chức năng kiểm tra username, nhưng trường hợp của form soạn thảo hơi khác một chút. Trong quá trình đăng ký, chúng ta cần bảo đảm rằng username mới nhập vào không tồn tại trong cơ sở dữ liệu. Trong form edit, chúng ta cũng cần làm tương tự như vậy, nhưng với một ngoại lệ: nếu user không thay đổi username, mã kiểm tra sẽ chấp nhận và không báo lỗi vì username đó đã được gán cho user đó. Sau đây là đoạn mã kiểm tra username trong form này:
  • app/forms.py: kiểm tra username trong form soạn thảo hồ sơ cá nhân
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.')
Chúng ta sẽ overload (nạp chồng phương thức) constructor của lớp EditProfileForm để nhận vào username hiện hành của user dưới dạng tham số. Username này sẽ được gán vào biến original_username của lớp và sẽ được dùng để so sánh với giá trị của username được nhập vào trong phương thức validate_username(). Nếu username được nhập vào giống với username hiện hành, chúng ta sẽ không cần kiểm tra trong cơ sở dữ liệu.

Để sử dụng phương thức kiểm tra mới này, chúng ta sẽ thêm username hiện hành vào hàm hiển thị tương ứng và dùng nó khi khởi tạo đối tượng form:
  • app/routes.py: kiểm tra username trong form soạn thảo hồ sơ cá nhân
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm(current_user.username)
    ...
Đến đây thì chúng ta đã gần như sửa xong lỗi này và nó sẽ không xảy ra trong phần lớn trường hợp. Tuy nhiên đây không phải là giải pháp tốt nhất vì nó có thể không làm việc đúng khi nhiều quá trình (process) cùng truy cập cơ sở dữ liệu cùng lúc. Khi đó, có thể sẽ xảy ra hiện tượng chạy đua (race condition): quá trình kiểm tra hợp lệ, nhưng khi chuẩn bị cập nhật dữ liệu trong cơ sở dữ liệu thì nó đã bị thay đổi bởi một quá trình khác và do đó, không thể thay đổi username. Điều này chỉ xảy ra với các ứng dụng phải xử lý rất nhiều yêu cầu cùng lúc, vì vậy tạm thời chúng ta không cần phải quan tâm về vấn đề này. Bây giờ bạn có thể thử lại một lần nữa để xem phương thức kiểm tra username mới xử lý lỗi này thế nào.

Getting Info...

Đăng nhận xét

Đồng ý cookie
Chúng tôi cung cấp cookie trên trang web này để phân tích lưu lượng truy cập, ghi nhớ các tùy chọn của bạn và tối ưu hóa trải nghiệm của bạn.
Oops!
Có vẻ như đã xảy ra sự cố với kết nối Internet của bạn. Vui lòng kết nối với internet và bắt đầu duyệt lại.
Trang web bị chặn
Xin lỗi! Trang web này không có sẵn ở quốc gia của bạn.