要做一个python web系统,做简单的信息管理和案例展示,还要考虑后续功能的扩展。就想着自己搭建一个python web框架。本来想着像.net、java一样,有现成的脚手架项目,拿来改吧改吧就用了,结果发现Python web这一块还真是奇葩:
- 很多python web应用或脚手架的维护3~4年前就已经停止维护了
- 只有python web的基础框架,像flask、Django,倒是维护的挺活跃。但是flask太轻,需要自己集成;Diango太重,想要的不想要的都给你集成在一起了。
本篇总结之-Web异常处理。
刚开始使用时flask异常模块时,业务处理中直接触发HTTPException异常,导致出现了莫名其妙的问题,异常无法正常捕获,最终复盘发现了原因。总结一下异常注册、异常触发、异常处理的流程,对比查看一下flask-jwt集成flask的异常实现。
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)
- 装饰器模式注册
@app.errorhandler(Exception)
def handle_error(e):
"""
Handle errors, formatting them as JSON if requested
"""
......
- 异常注册类型
一般Web应用异常分类HttpException异常(客户端异常)和Exception异常(服务端异常),注册的时候可以指定具体处理的异常类型,当然也可以统一处理。
- 基于蓝图注册异常
根据应用需要,也可以根据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.异常触发
- assert触发异常
assert语句又称作断言,指的是程序期望满足指定的条件。定义的约束条件不满足的时候,它会触发AssertionError异常,所以assert语句可以当作条件式的raise语句。
assert 逻辑表达式,data # data是可选的,通常是一个字符串,当表达式的结果为False时,作为异常类型的描述信息使用。
等同于
if not 逻辑表达式:
raise AssertionError(data)
- raise触发异常
raise [exceptionName [(reason)]]
# 其中,用 [] 括起来的为可选参数,其作用是指定抛出的异常名称,以及异常信息的相关描述。如果可选参数全部省略,则 raise 会把当前错误原样抛出;如果之前没有触发异常,触发RuntimeError;如果仅省略 (reason),则在抛出异常时,将不附带任何的异常描述信息。
- raise
- raise exceptionName
- raise exceptionName(reason)
- abort触发异常
在视图函数执行过程中,我们可以使用abort函数立即终止视图函数的执行。通过abort函数,可以返回一个app.aborter中存在的错误状态码,表示出现的错误信息。类似于python中raise。
abort(404)
- 不要直接触发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.异常处理
- 异常处理一般包括日志记录、堆栈记录方便错误排查。然后进行错误信息统一格式化,返回前端,方便前端操作人员理解系统故障。
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是怎么进行异常处理的
- 定义了一些自定义异常: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
- 定义每个异常的默认处理方法
这样的话就是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,
)
- 异常注册
这里就回到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)
...
- 自定义异常处理
当然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应用的实现机制真的是很容易。