Sanic 异常体系深度解析:SanicException 层次结构、错误处理与错误页渲染
【免费下载链接】sanicAccelerate your web app development | Build fast. Run fast.项目地址: https://gitcode.com/gh_mirrors/sa/sanic
本篇技术指南以 Sanic 官方 API 参考中的sanic.exceptions与sanic.errorpages两个模块(见 docs/sanic/api/exceptions.rst)为主线,系统讲解 Sanic 的异常类层次结构、异常属性、内置 HTTP 异常速查、ErrorHandler处理机制、HTML/Text/JSON 三种错误页渲染器以及FALLBACK_ERROR_FORMAT自动协商逻辑。读完本文,你将能够在 Sanic 应用中规范地抛出异常、自定义异常处理器与错误页格式,并理解异常信息在 DEBUG 与 PRODUCTION 两种模式下的展示差异。
一、异常体系总览:两个模块的分工
Sanic 的异常相关代码分布在两个模块中,二者共同构成了"抛异常 → 处理异常 → 渲染错误响应"的完整链路:
- sanic/exceptions.py:定义全部异常类。以
SanicException为根,派生HTTPException及一系列与 HTTP 状态码一一对应的标准异常(BadRequest、Unauthorized、Forbidden、NotFound、ServerError等)。 - sanic/errorpages.py:定义错误页渲染器(
BaseRenderer、HTMLRenderer、TextRenderer、JSONRenderer)以及格式协商函数guess_mime()与exception_response()。当异常未被自定义处理器拦截时,由这里的渲染器生成兜底(fallback)响应。
两者的连接点是 sanic/handlers/error.py 中的ErrorHandler类:它负责查表匹配用户注册的异常处理器,匹配不到时调用default()走 errorpages 渲染流程。
二、异常类层次结构
SanicException是所有异常的基类,其核心声明位于 sanic/exceptions.py:
BaseException └── CancelledError └── RequestCancelled └── Exception └── SanicException └── HTTPException ├── NotFound (404) ├── BadRequest (400) ← InvalidUsage / BadURL 别名 ├── MethodNotAllowed (405) ← MethodNotSupported 别名 ├── ServerError (500) ← InternalServerError 别名 ├── ServiceUnavailable (503) ├── URLBuildError (500) ├── FileNotFound (404,继承 NotFound) ├── RequestTimeout (408) ├── PayloadTooLarge (413) ├── HeaderNotFound (400,继承 BadRequest) ├── InvalidHeader (400,继承 BadRequest) ├── RangeNotSatisfiable (416) ← ContentRangeError 别名 ├── ExpectationFailed (417) ← HeaderExpectationFailed 别名 ├── Forbidden (403) ├── InvalidRangeType (416) └── Unauthorized (401)另外还有不属于 HTTP 状态码体系的特殊异常:ServerKilled、PyFileError、LoadFileException、InvalidSignal、WebsocketClosed,以及继承自asyncio.CancelledError的RequestCancelled。
SanicException 的构造逻辑
SanicException.__init__(sanic/exceptions.py)实现了几个关键行为:
message为None时,优先取类属性self.message,再退化为从STATUS_CODES表按status_code反查标准状态文本(如 500 → "Internal Server Error");bytes类型的消息会被自动decode()成str。status_code未显式传入时,回退到类属性status_code,最终默认 500。quiet、headers同样支持"实例传参优先、类属性兜底"的两级取值方式。context与extra直接存放在实例上,供渲染器在生成响应时读取。
三、SanicException 的六大核心属性
所有 Sanic 异常都派生自SanicException,该基类提供六个可在创建异常时传入、也可作为类变量预定义的属性(参见 guide/content/en/guide/best-practices/exceptions.md 的 "Exception properties" 一节):
| 属性 | 含义 | 用途建议 |
|---|---|---|
message | 发送给客户端的消息文本 | 在类上定义可统一全应用的错误文案 |
status_code | 随响应返回的 HTTP 状态码 | 定义自定义 4xx 系列异常时尤其常用 |
quiet | 为True时抑制 error_logger 的 traceback 输出 | 用异常触发处理器事件、不想刷日志时使用 |
headers | 附加到 HTTP 响应的请求头 | 从异常侧直接控制响应头 |
context | 附加键值数据,始终随响应发送给客户端 | 校验失败时给出替代值、展示登录用户状态等 |
extra | 附加键值数据,永不发送给生产环境客户端 | 动态生成错误消息、给 logger 传运行时细节 |
在类定义中固化属性
from sanic.exceptions import SanicException class TeapotError(SanicException): status_code = 418 message = "Sorry, I cannot brew coffee" raise TeapotError # 使用类默认值 raise TeapotError(status_code=400) # 实例级覆盖状态码 raise TeapotError("别急,我换个说法") # 实例级覆盖消息用 quiet 控制日志噪音
默认情况下,异常会被输出到error_logger。若你只是想用异常触发某个处理逻辑而不想留下 traceback,可设置quiet = True:
class SilentError(SanicException): message = "Something happened, but not shown in logs" quiet = True若在调试阶段希望全局忽略quiet=True强制输出所有异常日志,可将配置NOISY_EXCEPTIONS置为True(sanic/config.py 中默认值为False;ErrorHandler.log在 sanic/handlers/error.py 中读取该配置决定是否打印):
app.config.NOISY_EXCEPTIONS = True在异常中直接附加响应头
SanicException可以直接当作响应工具使用——不仅能控制状态码,还能直接控制响应头:
class MyException(SanicException): headers = {"X-Foo": "bar"} # 或按实例传入 raise MyException(headers={"X-Foo": "bar"})渲染器在生成响应时会通过BaseRenderer.headers属性(sanic/errorpages.py)取出这些头合并进最终响应。
四、内置标准异常速查表
Sanic 为最常见的 HTTP 错误预置了对应异常(源码见 sanic/exceptions.py),每个异常类自身携带status_code,并在多数场景下默认quiet = True:
| 异常类 | 状态码 | 说明 |
|---|---|---|
BadRequest | 400 | 客户端请求非法 |
Unauthorized | 401 | 未认证,支持拼接WWW-Authenticate头 |
Forbidden | 403 | 已认证但无权限 |
NotFound | 404 | 资源不存在 |
MethodNotAllowed | 405 | 方法不允许,自动生成Allow头 |
RequestTimeout | 408 | 请求超时(内部使用) |
PayloadTooLarge | 413 | 负载过大(内部使用) |
RangeNotSatisfiable | 416 | Range 请求不满足,自动生成Content-Range头 |
ExpectationFailed | 417 | Expect 头校验失败 |
ServerError | 500 | 通用服务端错误,应优先于裸SanicException使用 |
URLBuildError | 500 | Sanic 内部 URL 构建失败 |
ServiceUnavailable | 503 | 服务暂不可用 |
FileNotFound | 404 | 特定于文件系统查找失败的 404 |
官方指南建议你在业务中自行实现的最常用异常是:BadRequest(400)、Unauthorized(401)、Forbidden(403)、NotFound(404)、ServerError(500)。例如:
from sanic import exceptions @app.route("/login") async def login(request): user = await some_login_func(request) if not user: raise exceptions.NotFound( f"Could not find user with username={request.json.username}" )别名:更贴合语义的命名
源码中通过简单的赋值提供了语义化别名(sanic/exceptions.py、sanic/exceptions.py、sanic/exceptions.py、sanic/exceptions.py、sanic/exceptions.py),它们在身份上与原类是同一个对象,测试 tests/test_exceptions.py 专门验证了这一点:
InvalidUsage=BadRequest,BadURL=BadRequestMethodNotSupported=MethodNotAllowedInternalServerError=ServerErrorContentRangeError=RangeNotSatisfiableHeaderExpectationFailed=ExpectationFailed
带特殊行为的子类
MethodNotAllowed:构造时可传method与allowed_methods,后者会被拼成Allow: GET, POST之类的响应头(sanic/exceptions.py)。RangeNotSatisfiable:传入content_range协议对象后,自动写入Content-Range: bytes */<total>头(sanic/exceptions.py),服务于静态文件与 Range 下载场景(相关实现见 sanic/handlers/content_range.py)。Unauthorized:支持scheme与任意**challenges关键字参数,自动生成WWW-Authenticate响应头(sanic/exceptions.py):
# Basic 认证方案,realm 必须存在 raise Unauthorized( "Auth required.", scheme="Basic", realm="Restricted Area", ) # Bearer 方案,realm 可选 raise Unauthorized("Auth required.", scheme="Bearer") # Digest 方案,携带挑战参数 raise Unauthorized( "Auth required.", scheme="Digest", realm="Restricted Area", qop="auth, auth-int", algorithm="MD5", nonce="abcdef", opaque="zyxwvu", )FileNotFound:额外携带path与relative_url属性,用于静态文件/目录服务场景(sanic/exceptions.py)。PyFileError:在配置脚本无法执行时抛出,消息固定为could not execute config file <file>(sanic/exceptions.py)。WebsocketClosed:WebSocket 被客户端关闭时抛出,自带消息Client has closed the websocket connection且quiet = True(sanic/exceptions.py)。
五、异常处理:自定义处理器与 ErrorHandler
方式一:@app.exception()装饰器
Sanic 提供@app.exception()装饰器,不仅可以捕获 Sanic 标准异常,还能捕获应用内抛出的任意异常(装饰器实现在 sanic/mixins/exceptions.py):
from sanic.exceptions import NotFound from sanic.response import text @app.exception(NotFound, SomeCustomException) async def ignore_404s(request, exception): return text(f"Yep, I totally found the page: {request.url}")- 可一次传入多个异常类,也支持传入列表。
@app.exception(Exception)可作捕获一切的兜底处理器;@app.all_exceptions(sanic/mixins/exceptions.py)是其等价便捷写法。- 处理器可注册在 Blueprint 上,此时仅作用于该蓝图下的路由;而
NotFound这类通用异常官方建议只注册在应用实例上。
方式二:app.error_handler.add()
async def server_error_handler(request, exception): return text("Oops, server error", status=500) app.error_handler.add(Exception, server_error_handler)ErrorHandler.add()(sanic/handlers/error.py)内部把(异常类型, 路由名)写入cached_handlers字典;若对同一 (异常, 路由) 重复注册会抛出ServerError(sanic/handlers/error.py)。lookup()(sanic/handlers/error.py)在查找时不仅精确匹配类型,还会沿type.mro()向上查找父类处理器,因此为Exception注册的处理器能覆盖所有异常。response()(sanic/handlers/error.py)则负责实际执行处理器,若处理器自身抛出异常,debug 模式返回 500 文本,否则返回 "An error occurred while handling an error"。
方式三:继承 ErrorHandler 自定义默认行为
from sanic.handlers import ErrorHandler from sanic.response import json from sanic.request import Request from sanic.response.types import HTTPResponse class CustomErrorHandler(ErrorHandler): def default(self, request: Request, exception: Exception) -> HTTPResponse: # 处理所有未被注册处理器拦截的异常 status_code = getattr(exception, "status_code", 500) return json({"error": str(exception), "foo": "bar"}, status=status_code) app.error_handler = CustomErrorHandler()ErrorHandler在构造时接收一个base渲染器(默认TextRenderer),并持有debug标志——该标志会随应用的 debug 状态自动同步(见 sanic/application/state.py)。
六、错误页渲染:HTML / Text / JSON 三种格式
未捕获异常最终会走进ErrorHandler.default()(sanic/handlers/error.py),它读取app.config.FALLBACK_ERROR_FORMAT并调用exception_response()渲染兜底响应。渲染器类图如下(sanic/errorpages.py):
BaseRenderer ├── HTMLRenderer # text/html,默认兜底 ├── TextRenderer # text/plain └── JSONRenderer # application/jsonBaseRenderer.render()(sanic/errorpages.py)根据debug与异常的quiet属性在full(完整、含 traceback)与minimal(简洁、无敏感信息)两种输出之间切换:
- debug 为 True 且异常非 quiet:渲染
full版本,包含完整 traceback、请求路径、参数、异常链(__cause__链会逐层展开)。 - 否则:渲染
minimal版本。非SanicException的普通异常此时仅显示固定文案The application encountered an unexpected error and could not continue.(sanic/errorpages.py),避免向生产环境泄露内部细节。
FALLBACK_ERROR_FORMAT 的三种显式配置
app.config.FALLBACK_ERROR_FORMAT = "html" # 始终 HTML app.config.FALLBACK_ERROR_FORMAT = "text" # 始终纯文本 app.config.FALLBACK_ERROR_FORMAT = "json" # 始终 JSON在 sanic/config.py 中,FALLBACK_ERROR_FORMAT被实现为带 setter 的属性:一旦应用启动后修改该值会抛出异常提示,以保证运行时格式一致性。非法格式值会被check_error_format()(sanic/errorpages.py)拒绝。
各格式的响应示例
Text(debug)——curl localhost:8000/exc -i返回 500,正文包含标题栏、异常类型、路径及完整 traceback:
⚠️ 500 — Internal Server Error ============================== That time when that thing broke that other thing? That happened. ServerError: ... while handling path /exc Traceback of TestApp (most recent call last): ServerError: ... File /path/to/sanic/app.py, line 979, in handle_request response = await response ...Text(非 debug)——只剩标题与消息,无 traceback:
⚠️ 500 — Internal Server Error ============================== That time when that thing broke that other thing? That happened.JSON(debug)——结构化输出,含description、status、message、path、args与exceptions(每层含type、exception、frames):
{ "description": "Internal Server Error", "status": 500, "message": "That time when that thing broke that other thing? That happened.", "path": "/exc", "args": {}, "exceptions": [ { "type": "ServerError", "exception": "That time when that thing broke that other thing? That happened.", "frames": [ {"file": "/path/to/sanic/app.py", "line": 979, "name": "handle_request", "src": "response = await response"} ] } ] }JSON(非 debug)——只保留description、status、message三个字段。
按路由控制格式:error_format
除了全局配置,还可以在路由上通过error_format关键字按路由控制错误格式(Blueprints 会继承全局FALLBACK_ERROR_FORMAT,见 sanic/blueprints.py):
@app.route("/", error_format="text") async def handler(request): ...HTML 错误页的调试与生产差异
HTML 渲染由HTMLRenderer借助 sanic/pages/error.py 中的ErrorPage完成,样式定义在 sanic/pages/styles/ErrorPage.css。下图对比了 debug 开启与关闭时的 HTML 错误页差异:
七、Auto 模式:根据请求协商响应格式
设置app.config.FALLBACK_ERROR_FORMAT = "auto"可启用格式自动协商,这是 Sanic 的默认行为。guess_mime()(sanic/errorpages.py)按以下优先级决策:
- 路由的
error_format:若路由显式指定了格式(text/json/html),直接采用。 FALLBACK_ERROR_FORMAT:若配置为显式格式,采用之。- 请求线索:
auto模式下检查请求——若Accept头匹配application/json(字面匹配而非通配符),或Content-Type含application/json,则选择 JSON。旧版本还会尝试解析request.json推断(该行为已标记弃用并计划移除,见 sanic/errorpages.py)。 - Accept 头匹配:用
req.accept.match()在所有支持的 MIME 间协商(映射表MIME_BY_CONFIG/RENDERERS_BY_CONTENT_TYPE见 sanic/errorpages.py)。
直观效果:浏览器访问返回 HTML 错误页,curl或 API 客户端则可能收到 JSON 或纯文本。exception_response()(sanic/errorpages.py)根据协商结果实例化对应渲染器并产出最终响应。
八、Contextual Exceptions:context 与 extra 的正确用法
Sanic 的异常支持运行时附加键值数据(自 v21.12 起):
raise TeapotError(extra={"name": "Adam"}, context={"foo": "bar"})两者的关键区别在于是否发送给生产环境客户端:
extra:对象本身永不发送给生产客户端,仅供内部使用。典型场景:- 结合
@property message动态生成错误消息; - 向 logger 提供运行时细节;
- 开发模式下作为调试信息渲染。
- 结合
context:始终随响应发送给客户端。典型场景:- 在
BadRequest校验失败时给出可替代的合法取值; - 向客户返回便于开支持工单的补充信息;
- 展示当前登录用户等状态信息。
- 在
用 extra 动态生成消息
class TeapotError(SanicException): status_code = 418 @property def message(self): return f"Sorry {self.extra['name']}, I cannot make you coffee" raise TeapotError(extra={"name": "Adam"})用 context 向客户端传递信息
raise TeapotError(context={"foo": "bar"})非 debug 模式下 JSON 响应会包含context字段:
{ "description": "I'm a teapot", "status": 418, "message": "Sorry Adam, I cannot make you coffee", "context": {"foo": "bar"} }debug 模式则会额外附加path、args、exceptions(完整 traceback)与extra字段。context在 HTML 错误页中以exception-context定义列表展示、在 Text 输出中以Context小节展示,extra则在 debug 下以Extra小节展示(渲染逻辑见 sanic/errorpages.py 与 sanic/errorpages.py)。源码中的测试 tests/test_exceptions.py 对三种格式下的context/extra/动态消息行为做了逐项断言,例如extra在非 debug 下不会出现在 JSON 中、动态message可被实例级message覆盖等。
九、错误上报:report_exception 与信号
若希望把异常信息上报给 Sentry、Rollbar 等第三方服务,可以挂载report_exception处理器(自 v23.6 起),对应示例可参考仓库中的 examples/sentry_example.py 与 examples/rollbar_example.py:
@app.report_exception async def catch_any_exception(app: Sanic, exception: Exception): print("Caught exception:", exception)注意:该处理器会被调度进后台任务,仅用于日志与上报,不能用于修改返回给客户端的响应数据。从源码看,异常处理流程中还会派发http.lifecycle.exception信号(sanic/app.py),可用于监听所有请求生命周期内的异常。
十、测试与源码验证路径
以下文件可帮助你进一步深入验证本文所述机制:
- 异常类定义:sanic/exceptions.py
- 渲染器与格式协商:sanic/errorpages.py
- 处理器核心实现:sanic/handlers/error.py
- 装饰器实现:sanic/mixins/exceptions.py
- 配置项定义:sanic/config.py(
NOISY_EXCEPTIONS、FALLBACK_ERROR_FORMAT) - 异常在请求主流程中的处理位置:sanic/app.py
- 行为测试:tests/test_exceptions.py(别名、消息属性、quiet 属性、contextual exceptions、处理器异常等约 30 个用例)
- 官方使用指南:guide/content/en/guide/best-practices/exceptions.md
小结
Sanic 的异常体系是一条完整闭环:SanicException及标准子类负责以统一方式表达"发生了什么错误",context/extra负责携带结构化附加信息,ErrorHandler负责按注册表分发到自定义处理器,而errorpages中的三种渲染器与auto协商逻辑则保证未捕获异常也能以合适的格式、合适的信息量(debug 全量、生产精简)优雅地返回给客户端。掌握这套机制,你就能把异常从"不可控的报错"转变为"可预期、可观测、对客户端友好的业务响应"。
【免费下载链接】sanicAccelerate your web app development | Build fast. Run fast.项目地址: https://gitcode.com/gh_mirrors/sa/sanic
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考