مدیریت خطا های برنامه

برنامه ها کار نمیکنند، سرور ها از کار می‌افتند. دیر یا زود شاهد یک استثنا در ارائه محصول خواهید بود. حتی اگر کد شما ۱۰۰% درست باشد هم گاهی استثنا ها را خواهید دید. چرا؟ چون هر چیزی که پیچیده است شکست خواهد خورد. در اینجا مواردی وجود دارد که حتی با وجود کد های کاملا دقیق، میتواند منجر به خطا های سرور شود:

  • کلاینت درخواست را زودتر قطع کرد اما برنامه همچنان در حال خواندن داده های دریافتی بود

  • سرور دیتا‌بیس اضافه بار داشت و نتواند کوئری ها را مدیریت کند

  • یک فایل سیستم پر باشد

  • یک هارد‌ درایو خراب شود

  • به سرور بک‌اند اضافه بار وارد شود

  • یک ارور در کتابخانه ای که استفاده میکنید رخ دهد

  • اتصال شبکه سرور به سیستم دیگری انجام نگرفت

و این ها فقط چند نمونه کوچک از مشکلاتی است که ممکن است با آن روبرو شوید. پس چگونه با این نوع مشکلات برخورد کنیم؟ به صورت پیشفرض، اگر برنامه در حالت ارائه اجرا شود و یک استثنا مطرح شود، فلاسک صفحه بسیار ساده ای را براری شما نمایش می‌دهد و استثنا را در logger ثبت می‌کند.

ولی میتوانید کار های بیشتری انجام دهید. ما تنظیمات بهتری را برای مقابله با خطا ها از جمله استثنا های سفارشی و ابزار های شخص ثالث پوشش خواهیم داد.

ابزار های واقعه‌نگاری ارور ها

ارسال ایمیل های حاوی خطا، حتی اگر فقط برای ایمیل های مهم باشد، در صورتی که کاربران کافی به خطا برخورد کنند و فایل های گزارش نیز هرگز تا الان بررسی نشده باشند، میتواند طاقت‌فرسا شود. به همین دلیل است که توصیه میکنیم از Sentry برای مقابله با خطا های برنامه استفاده کنید. این به عنوان یک پروژه به صورت متن باز در گیت‌هاب در دسترس است و همچنین میتوانید یک نسخه برای میزبانی را به صورت رایگان امتحان کنید. Sentry خطا های تکراری را جمع آوری می‌کند، ردیابی پشته های کامل و متغیر های محلی را برای اشکال زدایی ضبط می‌کند و بر اساس خطا های جدید یا آستانه تکرار، ایمیل هایی را برای شما ارسال می‌کند.

برای استفاده از Sentry باید کلاینت sentry-sdk را با وابستگی اضافی flask نصب کنید.

$ pip install sentry-sdk[flask]

و سپس این را به برنامه فلاسک خود اضافه کنید:

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration

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

YOUR_DSN_HERE باید با مقدار DSN ای که از نصب Sentry دریافت می‌کنید، جایگزین شود.

پس از نصب، خرابی هایی که منجر به خطای سرور داخلی می‌شوند به طور خودکار به Sentry گزارش داده می شوند و از آنجا میتوانید اعلان های خطا را دریافت کنید.

همچنین ببینید:

کنترل کننده خطا ها

هنگامی که خطایی در فلاسک رخ می دهد، کد وضعیت HTTP مناسب برگردانده می شود. کد های ۴۰۰ تا ۴۹۹ خطاهایی را که در داده های درخواست کلاینت یا داده های درخواست شده وجود دارد نشان می دهد و ۵۰۰ تا ۵۹۹ خطاهای خود سرور یا برنامه را نشان می دهد.

ممکن است بخواهید در صورت بروز خطا، صفحات خطای سفارشی را به کاربر نشان دهید. این را می توان با ثبت کنترل کننده های خطا انجام داد.

کنترل کننده خطا تابعی است که وقتی یک نوع خطا مطرح می‌شود، پاسخی را برمی‌گرداند. این شبیه view تابعی است که وقتی 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. هندلرها را می توان برای یک کلاس خاص یا برای همه زیر کلاس های یک کلاس والد ثبت کرد.

مدیریت

هنگام ساختن یک برنامه فلاسک، با استثناهایی مواجه خواهید شد. اگر هنگام رسیدگی به درخواست، بخشی از کد شما خراب باشد (و هیچ کنترل کننده خطا ثبت نشده باشد)، یک "500 Internal Server Error" ( InternalServerError ) به طور پیش فرض برگردانده می شود. به طور مشابه، اگر درخواستی به یک مسیر ثبت نشده ارسال شود، خطای "404 Not Found" (NotFound) رخ می دهد. اگر مسیری یک روش درخواست غیرمجاز دریافت کند، یک "405 Method Not Allowed" (MethodNotAllowed) برگردانده می شود. اینها همه زیر کلاس های HTTPException هستند و به طور پیش فرض در فلاسک ارائه می شوند.

فلاسک به شما این امکان را می دهد که هر استثنای HTTP ثبت شده توسط Werkzeug را افزایش دهید. با این حال، استثناهای HTTP پیش‌فرض، صفحات استثنای ساده را برمی‌گردانند. ممکن است بخواهید در صورت بروز خطا، صفحات خطای سفارشی را به کاربر نشان دهید. این را می توان با ثبت کنترل کننده های خطا انجام داد.

هنگامی که فلاسک در حین رسیدگی به یک درخواست، استثنایی را می گیرد، ابتدا با کد جست و جو میشود. اگر هیچ کنترل کننده ای برای کد ثبت نشده باشد، فلاسک خطا را بر اساس سلسله مراتب کلاس خود جستجو می کند. خاص ترین کنترل کننده انتخاب می شود. اگر کنترل‌کننده‌ای ثبت نشده باشد، زیر کلاس‌های HTTPException یک پیام عمومی در مورد کد خود نشان می‌دهند، در حالی که سایر استثناها به یک "500 Internal Server Error" عمومی تبدیل می‌شوند.

به عنوان مثال، اگر نمونه ای از ConnectionRefusedError مطرح شود، و یک کنترل کننده برای ConnectionError و ConnectionRefusedError ثبت شود، کنترل کننده خاص تر HTTPException با نمونه استثنایی برای تولید پاسخ است.

کنترل‌کننده‌های ثبت‌شده در طرح اولیه بر آن‌هایی که در سطح سراسری(global) در برنامه ثبت شده‌اند، اولویت دارند، با این فرض که یک طرح در حال رسیدگی به درخواستی است که استثنا را ایجاد می‌کند. با این حال، طرح اولیه نمی تواند خطاهای مسیریابی 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 ممکن است برای تغییر نحوه ارائه همه خطاها، حتی خطاهای کنترل نشده، به کاربر مفید به نظر برسد. با این حال، این شبیه به انجام 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

Error handlers still respect the exception class hierarchy. If you register handlers for both HTTPException and Exception, the Exception handler will not handle HTTPException subclasses because the HTTPException handler is more specific.

استثناهای کنترل نشده

هنگامی که کنترل کننده خطا برای یک استثنا ثبت نشده است، به جای آن یک خطای سرور داخلی 500 برگردانده می شود. برای اطلاعات در مورد این رفتار به flask.Flask.handle_exception() مراجعه کنید.

اگر یک کنترل کننده خطا برای InternalServerError ثبت شده باشد، این مورد فراخوانی می شود. از فلاسک 1.1.0، این کنترل کننده خطا همیشه یک نمونه از InternalServerError ارسال می شود، نه خطای کنترل نشده اصلی.

خطای اصلی به عنوان e.original_exception موجود است.

یک کنترل کننده خطا برای "500 Internal Server Error" علاوه بر خطاهای ۵۰۰ صریح، به استثناهای غیرقابل کشف منتقل می شود. در حالت اشکال زدایی، یک کنترل کننده برای "500 Internal Server Error" استفاده نخواهد شد. در عوض، دیباگر تعاملی نشان داده خواهد شد.

صفحات خطای سفارشی

گاهی اوقات هنگام ساختن یک برنامه فلاسک، ممکن است بخواهید یک HTTPException را مطرح کنید تا به کاربر علامت دهد که مشکلی در درخواست وجود دارد. خوشبختانه، فلاسک دارای یک تابع مفید abort() است که درخواستی با خطای HTTP از werkzeug را به دلخواه لغو می کند. همچنین یک صفحه خطای سیاه و سفید ساده با توضیحات اولیه برای شما ارائه می دهد، اما هیچ چیز جالبی نیست.

بسته به کد خطا، احتمال اینکه کاربر واقعاً چنین خطایی را ببیند، کم یا زیاد است.

کد زیر را در نظر بگیرید، ممکن است یک مسیر پروفایل کاربری داشته باشیم، و اگر کاربر نتواند یک نام کاربری را ارسال کند، می توانیم یک "400 Bad Request" ارسال کنیم. اگر کاربر یک نام کاربری را ارسال کند و ما نتوانیم آن را پیدا کنیم، "404 Not Found" را بر می گردانیم.

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 Page Not Found" آمده است:

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 Internal Server Error" پیاده سازی کرد:

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

هنگام استفاده از Modular Applications with Blueprints :

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)

کنترل کننده های خطا در Blueprint

در Modular Applications with Blueprints ، اکثر کنترل کننده های خطا همانطور که انتظار می رود کار می کنند. با این حال، یک هشدار در مورد کنترل کننده ها برای استثناهای 404 و 405 وجود دارد. این کنترل‌کننده‌های خطا فقط از یک عبارت raise مناسب یا فراخوانی abort در یکی از توابع blueprint فراخوانی می‌شوند نه توسط دسترسی به 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

برگرداندن خطاهای API به صورت JSON

هنگام ساختن 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())

اکنون یک view می تواند آن استثنا را با یک پیام خطا ایجاد کند. علاوه بر این، برخی از محموله های اضافی را می توان به عنوان dictionary از طریق پارامتر payload ارائه کرد.

لاگ کردن

برای اطلاعات در مورد نحوه ثبت استثناها، مانند ارسال ایمیل به مدیران، به لاگ کردن مراجعه کنید.

اشکال زدایی

برای اطلاعات در مورد نحوه اشکال زدایی خطاها در توسعه و تولید به اشکال زدایی خطاهای برنامه مراجعه کنید.