Python Web异常处理


要做一个python web系统,做简单的信息管理和案例展示,还要考虑后续功能的扩展。就想着自己搭建一个python web框架。本来想着像.net、java一样,有现成的脚手架项目,拿来改吧改吧就用了,结果发现Python web这一块还真是奇葩:

  1. 很多python web应用或脚手架的维护3~4年前就已经停止维护了
  2. 只有python web的基础框架,像flask、Django,倒是维护的挺活跃。但是flask太轻,需要自己集成;Diango太重,想要的不想要的都给你集成在一起了。

本篇总结之-Web异常处理

刚开始使用时flask异常模块时,业务处理中直接触发HTTPException异常,导致出现了莫名其妙的问题,异常无法正常捕获,最终复盘发现了原因。总结一下异常注册、异常触发、异常处理的流程,对比查看一下flask-jwt集成flask的异常实现。

1.异常处理流程

1.异常注册
  1. 工厂模式注册
def create_app(config_name="default"):
    app = Flask(__name__)
    app.config.from_object(config_by_name[config_name])

    register_extensions(app)

    # Register error handler
    app.register_error_handler(Exception, handle_error)

def handle_error(e):
    """
        Handle errors, formatting them as JSON if requested
        """
    message = str(e)
    trace = None
    description = None
    status_code = 500
    if isinstance(e, werkzeug.exceptions.HTTPException):
        status_code = e.code
        description = e.description
    if current_app.debug:
        trace = traceback.format_exc()

    details = {
        'message': message,
        'type': error_type,
    }
    if description is not None:
        details['description'] = description
    if trace is not None:
        details['trace'] = trace.split('\n')
    return fail_api(message, status_code, details)
  1. 装饰器模式注册
@app.errorhandler(Exception)
def handle_error(e):
    """
        Handle errors, formatting them as JSON if requested
        """
    ......
  1. 异常注册类型

一般Web应用异常分类HttpException异常(客户端异常)和Exception异常(服务端异常),注册的时候可以指定具体处理的异常类型,当然也可以统一处理。

  1. 基于蓝图注册异常

根据应用需要,也可以根据blueprint进行异常注册处理。既可以把异常注册到具体的blueprint上,也可以把异常注册到全局的flask app上。flask在寻找异常处理程序时,优先查找注册在blueprint上的处理器。当一个蓝图在处理抛出异常的请求时,在蓝图中注册的出错处理器优先于在应用中全局注册的出错处理器。但是,蓝图无法处理404路由错误,因为404发生的路由级别还不能检测到蓝图。

def _find_error_handler(self, e: Exception) -> t.Optional[ft.ErrorHandlerCallable]:
    """Return a registered error handler for an exception in this order:
    blueprint handler for a specific code, app handler for a specific code,
    blueprint handler for an exception class, app handler for an exception
    class, or ``None`` if a suitable handler is not found.
    """
    exc_class, code = self._get_exc_class_and_code(type(e))
    names = (*request.blueprints, None)

    for c in (code, None) if code is not None else (None,):
        for name in names:
            handler_map = self.error_handler_spec[name][c]

            if not handler_map:
                continue

            for cls in exc_class.__mro__:
                handler = handler_map.get(cls)

                if handler is not None:
                    return handler
    return None
2.异常触发
  1. assert触发异常

assert语句又称作断言,指的是程序期望满足指定的条件。定义的约束条件不满足的时候,它会触发AssertionError异常,所以assert语句可以当作条件式的raise语句。

assert 逻辑表达式,data      # data是可选的,通常是一个字符串,当表达式的结果为False时,作为异常类型的描述信息使用。

等同于

if not 逻辑表达式:
   raise AssertionError(data)
  1. raise触发异常
raise [exceptionName [(reason)]] 
# 其中,用 [] 括起来的为可选参数,其作用是指定抛出的异常名称,以及异常信息的相关描述。如果可选参数全部省略,则 raise 会把当前错误原样抛出;如果之前没有触发异常,触发RuntimeError;如果仅省略 (reason),则在抛出异常时,将不附带任何的异常描述信息。
  • raise
  • raise exceptionName
  • raise exceptionName(reason)
  1. abort触发异常

在视图函数执行过程中,我们可以使用abort函数立即终止视图函数的执行。通过abort函数,可以返回一个app.aborter中存在的错误状态码,表示出现的错误信息。类似于python中raise。

abort(404)
  1. 不要直接触发HTTPException异常

不要直接触发HttpException异常,这样会导致错误不能被正常捕获处理。
首先flask会单独判断一下HTTPException异常,如果是的话转入handle_http_exeption处理器进行处理。然后handle_http_exeption处理逻辑中,会默认访问code属性。但是我们直接触发HTTPException时是无法设置code属性的。因此这时会引发新的异常,导致错误处理不会走我们注册的处理程序,造成莫名其妙的错误。我们要么触发类似NotFound这些基于HTTPException扩展的异常,要么可以基于HTTPException继承自定义的异常。

def handle_user_exception(
    self, e: Exception
) -> t.Union[HTTPException, ft.ResponseReturnValue]:
    """This method is called whenever an exception occurs that
    should be handled. A special case is :class:`~werkzeug
    .exceptions.HTTPException` which is forwarded to the
    :meth:`handle_http_exception` method. This function will either
    return a response value or reraise the exception with the same
    traceback.

    .. versionchanged:: 1.0
        Key errors raised from request data like ``form`` show the
        bad key in debug mode rather than a generic bad request
        message.

    .. versionadded:: 0.7
    """
    if isinstance(e, BadRequestKeyError) and (
        self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"]
    ):
        e.show_exception = True

    if isinstance(e, HTTPException) and not self.trap_http_exception(e):
        return self.handle_http_exception(e)

    handler = self._find_error_handler(e)

    if handler is None:
        raise

    return self.ensure_sync(handler)(e)
def handle_http_exception(
    self, e: HTTPException
) -> t.Union[HTTPException, ft.ResponseReturnValue]:
    """Handles an HTTP exception.  By default this will invoke the
    registered error handlers and fall back to returning the
    exception as response.

    .. versionchanged:: 1.0.3
        ``RoutingException``, used internally for actions such as
            slash redirects during routing, is not passed to error
            handlers.

    .. versionchanged:: 1.0
        Exceptions are looked up by code *and* by MRO, so
        ``HTTPException`` subclasses can be handled with a catch-all
        handler for the base ``HTTPException``.

    .. versionadded:: 0.3
    """
    # Proxy exceptions don't have error codes.  We want to always return
    # those unchanged as errors
    if e.code is None:
        return e

    # RoutingExceptions are used internally to trigger routing
    # actions, such as slash redirects raising RequestRedirect. They
    # are not raised or handled in user code.
    if isinstance(e, RoutingException):
        return e

    handler = self._find_error_handler(e)
    if handler is None:
        return e
    return self.ensure_sync(handler)(e)
3.异常处理
  1. 异常处理一般包括日志记录、堆栈记录方便错误排查。然后进行错误信息统一格式化,返回前端,方便前端操作人员理解系统故障。
def handle_error(e):
    """
        Handle errors, formatting them as JSON if requested
        """
    # log stack info
    format_exec_info = traceback.format_exc()
    if format_exec_info:
        current_app.logger.error(format_exec_info)

    error_type = type(e).__name__
    message = str(e)
    trace = None
    description = None
    status_code = 500
    if isinstance(e, werkzeug.exceptions.HTTPException):
        status_code = e.code
        description = e.description
    if current_app.debug:
        trace = traceback.format_exc()

    details = {
        'message': message,
        'type': error_type,
    }
    if description is not None:
        details['description'] = description
    if trace is not None:
        details['trace'] = trace.split('\n')
    return fail_api(message, status_code, details)

2.flask-jwt集成,认证相关异常处理

我们看看flask-jwt是怎么进行异常处理的

  1. 定义了一些自定义异常:CSRFError、DecodeError、FreshTokenRequired、MissingRequiredClaimError等。
class JWTExtendedException(Exception):
    """
    Base except which all flask_jwt_extended errors extend
    """

    pass


class JWTDecodeError(JWTExtendedException):
    """
    An error decoding a JWT
    """

    pass
  1. 定义每个异常的默认处理方法

这样的话就是flask-jwt框架可以有一个默认的异常处理行为,在我们没有注册自己的异常处理方法时,由框架定义的默认处理方法进行处理。

self._decode_key_callback = default_decode_key_callback
self._encode_key_callback = default_encode_key_callback
self._expired_token_callback = default_expired_token_callback
self._invalid_token_callback = default_invalid_token_callback
self._jwt_additional_header_callback = default_jwt_headers_callback
self._needs_fresh_token_callback = default_needs_fresh_token_callback
self._revoked_token_callback = default_revoked_token_callback
self._token_in_blocklist_callback = default_blocklist_callback
self._token_verification_callback = default_token_verification_callback
self._unauthorized_callback = default_unauthorized_callback
self._user_claims_callback = default_additional_claims_callback
self._user_identity_callback = default_user_identity_callback
self._user_lookup_callback: Optional[Callable] = None
self._user_lookup_error_callback = default_user_lookup_error_callback
self._token_verification_failed_callback = (
    default_token_verification_failed_callback
)

def default_invalid_token_callback(error_string: str) -> ResponseReturnValue:
    """
    By default, if an invalid token attempts to access a protected endpoint, we
    return the error string for why it is not valid with a 422 status code

    :param error_string: String indicating why the token is invalid
    """
    return (
        jsonify({config.error_msg_key: error_string}),
        HTTPStatus.UNPROCESSABLE_ENTITY,
    )
  1. 异常注册

这里就回到flask的异常注册了。这里把flask-jwt自定义的异常行为和自定义的异常回调注册到flask异常处理模块。由于flask在查找异常处理器时,优先子类异常,因此这些自定义的异常会优先查找处理器,也就把这些flask-jwt自定义异常的处理权限交给了框架处理。

def _set_error_handler_callbacks(self, app: Flask) -> None:
    @app.errorhandler(CSRFError)
    def handle_csrf_error(e):
        return self._unauthorized_callback(str(e))

    @app.errorhandler(DecodeError)
    def handle_decode_error(e):
        return self._invalid_token_callback(str(e))

    @app.errorhandler(ExpiredSignatureError)
    def handle_expired_error(e):
        return self._expired_token_callback(e.jwt_header, e.jwt_data)

    ...
  1. 自定义异常处理

当然flask-jwt也提供了我们处理这些自定义异常的入口,这样我们可以自定义如何处理这些flask-jwt定义的异常。如果我们需要接管flask-jwt这些自定义异常的处理,就要按照flask-jwt定义的装饰器,逐个注册处理器,用来覆盖默认的处理器。我这里注册了未授权处理器、无效token处理器,这个可以根据自己的项目需要。

@jwt.unauthorized_loader
def unauthorized_callback(callback):
    return permission_error(callback)


@jwt.invalid_token_loader
def invalid_token_callback(callback):
    return permission_error(callback)


@jwt.token_verification_failed_loader
def token_verification_failed(callback):
    return permission_error(callback)

总的来说,python框架真的是短小精悍,却面面俱到,用来学习各种web应用的实现机制真的是很容易。

参考博客

flask 全局异常处理
Flask开发技巧之异常处理
应用错误处理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱在一瞬间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值