處理應用程式錯誤

應用程式會出錯,伺服器也會出錯。遲早您會在生產環境中看到例外。即使您的程式碼 100% 正確,您仍然會不時看到例外。為什麼?因為其他所有相關事物都可能出錯。以下是一些即使程式碼完全沒問題,仍可能導致伺服器錯誤的情況:

  • 用戶端過早終止請求,而應用程式仍在讀取傳入的資料

  • 資料庫伺服器過載,無法處理查詢

  • 檔案系統已滿

  • 硬碟故障

  • 後端伺服器過載

  • 您正在使用的函式庫中出現程式錯誤

  • 伺服器與另一個系統的網路連線失敗

而這僅是您可能面臨問題的一小部分範例。那麼我們該如何處理這類問題呢?預設情況下,如果您的應用程式在生產模式下執行,並且發生例外,Flask 會為您顯示一個非常簡單的頁面,並將例外記錄到 logger

但您可以做得更多,我們將介紹一些更好的設定來處理錯誤,包括自訂例外和第三方工具。

錯誤記錄工具

如果足夠多的使用者遇到錯誤,並且記錄檔通常從未被查看,那麼即使僅針對嚴重錯誤發送錯誤郵件也可能會變得難以負荷。這就是為什麼我們建議使用 Sentry 來處理應用程式錯誤。它作為原始碼可用的專案 在 GitHub 上 提供,並且也以 託管版本 提供,您可以免費試用。Sentry 會彙整重複的錯誤、擷取完整的堆疊追蹤和本機變數以進行偵錯,並根據新錯誤或頻率閾值向您發送郵件。

若要使用 Sentry,您需要安裝具有額外 flask 相依性的 sentry-sdk 用戶端。

$ pip install sentry-sdk[flask]

然後將此新增至您的 Flask 應用程式

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration

sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()])

YOUR_DSN_HERE 值需要替換為您從 Sentry 安裝取得的 DSN 值。

安裝後,導致內部伺服器錯誤的失敗會自動報告給 Sentry,您可以從那裡接收錯誤通知。

另請參閱

錯誤處理常式

當 Flask 中發生錯誤時,將會傳回適當的 HTTP 狀態碼。400-499 表示用戶端請求資料或關於請求資料的錯誤。500-599 表示伺服器或應用程式本身的錯誤。

您可能希望在發生錯誤時向使用者顯示自訂錯誤頁面。這可以透過註冊錯誤處理常式來完成。

錯誤處理常式是一個函式,當引發某種類型的錯誤時,它會傳回回應,類似於視圖是一個函式,當請求 URL 符合時,它會傳回回應。它會傳遞正在處理的錯誤實例,這很可能是 HTTPException

回應的狀態碼不會設定為處理常式的程式碼。請確保從處理常式傳回回應時提供適當的 HTTP 狀態碼。

註冊

透過使用 errorhandler() 修飾器裝飾函式來註冊處理常式。或者使用 register_error_handler() 稍後註冊函式。請記住在傳回回應時設定錯誤代碼。

@app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_bad_request(e):
    return 'bad request!', 400

# or, without the decorator
app.register_error_handler(400, handle_bad_request)

werkzeug.exceptions.HTTPException 子類別,例如 BadRequest 及其 HTTP 代碼在註冊處理常式時可以互換。(BadRequest.code == 400

非標準 HTTP 代碼無法透過代碼註冊,因為 Werkzeug 不知道它們。相反地,請定義 HTTPException 的子類別,並使用適當的代碼,然後註冊並引發該例外類別。

class InsufficientStorage(werkzeug.exceptions.HTTPException):
    code = 507
    description = 'Not enough storage space.'

app.register_error_handler(InsufficientStorage, handle_507)

raise InsufficientStorage()

可以為任何例外類別註冊處理常式,而不僅僅是 HTTPException 子類別或 HTTP 狀態碼。可以為特定類別或父類別的所有子類別註冊處理常式。

處理

在建置 Flask 應用程式時,您遇到例外。如果在處理請求時您的部分程式碼中斷(且您沒有註冊任何錯誤處理常式),則預設會傳回「500 內部伺服器錯誤」(InternalServerError)。同樣地,如果請求傳送到未註冊的路由,則會發生「404 找不到」(NotFound)錯誤。如果路由收到不允許的請求方法,則會引發「405 方法不允許」(MethodNotAllowed)。這些都是 HTTPException 的子類別,並且預設在 Flask 中提供。

Flask 讓您能夠引發 Werkzeug 註冊的任何 HTTP 例外。但是,預設的 HTTP 例外會傳回簡單的例外頁面。您可能希望在發生錯誤時向使用者顯示自訂錯誤頁面。這可以透過註冊錯誤處理常式來完成。

當 Flask 在處理請求時捕獲到例外時,它首先會按代碼查找。如果沒有為該代碼註冊處理常式,Flask 會按其類別階層查找錯誤;選擇最特定的處理常式。如果沒有註冊處理常式,HTTPException 子類別會顯示關於其代碼的通用訊息,而其他例外則會轉換為通用的「500 內部伺服器錯誤」。

例如,如果引發了 ConnectionRefusedError 的實例,並且為 ConnectionErrorConnectionRefusedError 註冊了處理常式,則會呼叫更特定的 ConnectionRefusedError 處理常式,並傳遞例外實例以產生回應。

假設藍圖正在處理引發例外的請求,則在藍圖上註冊的處理常式優先於在應用程式上全域註冊的處理常式。但是,藍圖無法處理 404 路由錯誤,因為 404 發生在路由層級,然後才能確定藍圖。

通用例外處理常式

可以為非常通用的基底類別(例如 HTTPException 甚至 Exception)註冊錯誤處理常式。但是,請注意,這些會捕獲比您預期更多的錯誤。

例如,HTTPException 的錯誤處理常式可能適用於將預設 HTML 錯誤頁面轉換為 JSON。但是,此處理常式將針對您未直接導致的事情觸發,例如路由期間的 404 和 405 錯誤。請務必仔細設計您的處理常式,以免丟失有關 HTTP 錯誤的資訊。

from flask import json
from werkzeug.exceptions import HTTPException

@app.errorhandler(HTTPException)
def handle_exception(e):
    """Return JSON instead of HTML for HTTP errors."""
    # start with the correct headers and status code from the error
    response = e.get_response()
    # replace the body with JSON
    response.data = json.dumps({
        "code": e.code,
        "name": e.name,
        "description": e.description,
    })
    response.content_type = "application/json"
    return response

Exception 的錯誤處理常式似乎可用於變更所有錯誤(甚至是未處理的錯誤)向使用者呈現的方式。但是,這類似於在 Python 中執行 except Exception:,它將捕獲所有其他未處理的錯誤,包括所有 HTTP 狀態碼。

在大多數情況下,為更特定的例外註冊處理常式會更安全。由於 HTTPException 實例是有效的 WSGI 回應,您也可以直接傳遞它們。

from werkzeug.exceptions import HTTPException

@app.errorhandler(Exception)
def handle_exception(e):
    # pass through HTTP errors
    if isinstance(e, HTTPException):
        return e

    # now you're handling non-HTTP exceptions only
    return render_template("500_generic.html", e=e), 500

錯誤處理常式仍然遵循例外類別階層。如果您同時為 HTTPExceptionException 註冊處理常式,則 Exception 處理常式將不會處理 HTTPException 子類別,因為 HTTPException 處理常式更具體。

未處理的例外

當沒有為例外註冊錯誤處理常式時,將改為傳回 500 內部伺服器錯誤。請參閱 flask.Flask.handle_exception() 以取得有關此行為的資訊。

如果為 InternalServerError 註冊了錯誤處理常式,則將會調用它。從 Flask 1.1.0 開始,此錯誤處理常式將始終傳遞 InternalServerError 的實例,而不是原始的未處理錯誤。

原始錯誤可用作 e.original_exception

「500 內部伺服器錯誤」的錯誤處理常式除了明確的 500 錯誤外,還將傳遞未捕獲的例外。在偵錯模式下,將不會使用「500 內部伺服器錯誤」的處理常式。而是會顯示互動式偵錯工具。

自訂錯誤頁面

有時在建置 Flask 應用程式時,您可能想要引發 HTTPException,以向使用者發出請求有問題的訊號。幸運的是,Flask 帶有一個方便的 abort() 函式,可根據需要使用 werkzeug 中的 HTTP 錯誤中止請求。它還將為您提供一個純黑白錯誤頁面,其中包含基本描述,但沒有任何花俏的東西。

根據錯誤代碼,使用者實際看到此類錯誤的可能性較低或較高。

考慮以下程式碼,我們可能有使用者個人資料路由,如果使用者未能傳遞使用者名稱,我們可以引發「400 錯誤請求」。如果使用者傳遞了使用者名稱,但我們找不到它,我們將引發「404 找不到」。

from flask import abort, render_template, request

# a username needs to be supplied in the query args
# a successful request would be like /profile?username=jack
@app.route("/profile")
def user_profile():
    username = request.arg.get("username")
    # if a username isn't supplied in the request, return a 400 bad request
    if username is None:
        abort(400)

    user = get_user(username=username)
    # if a user can't be found by their username, return 404 not found
    if user is None:
        abort(404)

    return render_template("profile.html", user=user)

以下是「404 頁面未找到」例外的另一個範例實作

from flask import render_template

@app.errorhandler(404)
def page_not_found(e):
    # note that we set the 404 status explicitly
    return render_template('404.html'), 404

當使用 應用程式工廠

from flask import Flask, render_template

def page_not_found(e):
  return render_template('404.html'), 404

def create_app(config_filename):
    app = Flask(__name__)
    app.register_error_handler(404, page_not_found)
    return app

範例範本可能是這樣的

{% extends "layout.html" %}
{% block title %}Page Not Found{% endblock %}
{% block body %}
  <h1>Page Not Found</h1>
  <p>What you were looking for is just not there.
  <p><a href="{{ url_for('index') }}">go somewhere nice</a>
{% endblock %}

更多範例

以上範例實際上並不會改進預設的例外頁面。我們可以建立一個自訂的 500.html 範本,如下所示

{% extends "layout.html" %}
{% block title %}Internal Server Error{% endblock %}
{% block body %}
  <h1>Internal Server Error</h1>
  <p>Oops... we seem to have made a mistake, sorry!</p>
  <p><a href="{{ url_for('index') }}">Go somewhere nice instead</a>
{% endblock %}

可以透過在「500 內部伺服器錯誤」上呈現範本來實作

from flask import render_template

@app.errorhandler(500)
def internal_server_error(e):
    # note that we set the 500 status explicitly
    return render_template('500.html'), 500

當使用 應用程式工廠

from flask import Flask, render_template

def internal_server_error(e):
  return render_template('500.html'), 500

def create_app():
    app = Flask(__name__)
    app.register_error_handler(500, internal_server_error)
    return app

當使用 具有藍圖的模組化應用程式

from flask import Blueprint

blog = Blueprint('blog', __name__)

# as a decorator
@blog.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

# or with register_error_handler
blog.register_error_handler(500, internal_server_error)

藍圖錯誤處理常式

具有藍圖的模組化應用程式 中,大多數錯誤處理常式將如預期般運作。但是,關於 404 和 405 例外的處理常式有一個注意事項。這些錯誤處理常式僅從藍圖的另一個視圖函式中的適當 raise 語句或 abort 呼叫中調用;它們不是由例如無效的 URL 存取調用的。

這是因為藍圖不「擁有」特定的 URL 空間,因此應用程式實例無法知道如果給定無效的 URL,它應該執行哪個藍圖錯誤處理常式。如果您想根據 URL 前綴為這些錯誤執行不同的處理策略,則可以在應用程式層級使用 request 代理物件來定義它們。

from flask import jsonify, render_template

# at the application level
# not the blueprint level
@app.errorhandler(404)
def page_not_found(e):
    # if a request is in our blog URL space
    if request.path.startswith('/blog/'):
        # we return a custom blog 404 page
        return render_template("blog/404.html"), 404
    else:
        # otherwise we return our generic site-wide 404 page
        return render_template("404.html"), 404

@app.errorhandler(405)
def method_not_allowed(e):
    # if a request has the wrong method to our API
    if request.path.startswith('/api/'):
        # we return a json saying so
        return jsonify(message="Method Not Allowed"), 405
    else:
        # otherwise we return a generic site-wide 405 page
        return render_template("405.html"), 405

以 JSON 格式傳回 API 錯誤

在 Flask 中建置 API 時,一些開發人員意識到內建例外對於 API 來說不夠具表現力,並且它們發出的 text/html 內容類型對於 API 消費者來說不是很有用。

使用與上述相同的技術和 jsonify(),我們可以將 JSON 回應傳回給 API 錯誤。abort() 會使用 description 參數呼叫。錯誤處理常式將使用它作為 JSON 錯誤訊息,並將狀態碼設定為 404。

from flask import abort, jsonify

@app.errorhandler(404)
def resource_not_found(e):
    return jsonify(error=str(e)), 404

@app.route("/cheese")
def get_one_cheese():
    resource = get_resource()

    if resource is None:
        abort(404, description="Resource not found")

    return jsonify(resource)

我們也可以建立自訂例外類別。例如,我們可以為 API 引入一個新的自訂例外,它可以接受適當的易於理解的訊息、錯誤的狀態碼和一些可選的酬載,以提供更多錯誤的上下文。

這是一個簡單的範例

from flask import jsonify, request

class InvalidAPIUsage(Exception):
    status_code = 400

    def __init__(self, message, status_code=None, payload=None):
        super().__init__()
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

@app.errorhandler(InvalidAPIUsage)
def invalid_api_usage(e):
    return jsonify(e.to_dict()), e.status_code

# an API app route for getting user information
# a correct request might be /api/user?user_id=420
@app.route("/api/user")
def user_api(user_id):
    user_id = request.arg.get("user_id")
    if not user_id:
        raise InvalidAPIUsage("No user id provided!")

    user = get_user(user_id=user_id)
    if not user:
        raise InvalidAPIUsage("No such user!", status_code=404)

    return jsonify(user.to_dict())

現在,視圖可以使用錯誤訊息引發該例外。此外,可以透過 payload 參數以字典形式提供一些額外的酬載。

記錄

請參閱 記錄 以取得有關如何記錄例外的資訊,例如透過電子郵件將其發送給管理員。

偵錯

請參閱 偵錯應用程式錯誤 以取得有關如何在開發和生產環境中偵錯錯誤的資訊。