没有错误处理时,访问不存在的页面显示默认 404(英文、简陋);服务器内部错误显示 debug 页面(生产环境会泄露源码)。好的错误处理:404 显示友好的"页面不存在",500 显示"出错了,请稍后再试",API 返回结构化的错误 JSON。
💡 关键直觉:错误处理是"应用的门面"。用户可能没看过你的成功页面,但一定会在出错时看到错误页——它决定了用户对应用的印象。
from flask import Flask, render_template app = Flask(__name__) @app.errorhandler(404) def not_found(e): return render_template('404.html'), 404 @app.errorhandler(500) def internal_error(e): return render_template('500.html'), 500
@app.errorhandler(404) 注册"当出现 404 时执行此函数"。函数返回响应与状态码。
from flask import abort @app.route('/admin') def admin(): # 权限不足时主动返回 403 abort(403) @app.route('/user/<int:user_id>') def user_detail(user_id): if user_id <= 0: abort(404) # 无效 ID 返回 404 return f"用户 {user_id}"
abort 的作用:在视图里"主动制造"一个错误码,交给 errorhandler 处理——视图只管判断,错误渲染统一交给处理器。
class BusinessError(Exception): """业务异常""" def __init__(self, message, code=400): self.message = message self.code = code super().__init__(message) @app.errorhandler(BusinessError) def handle_business_error(e): return {"error": e.message, "code": e.code}, e.code @app.route('/transfer') def transfer(): # 业务校验失败时抛业务异常 if not has_enough_balance(): raise BusinessError("余额不足", 400) return "转账成功"
自定义异常的价值:把"业务规则失败"(余额不足、重复注册)与"系统错误"(500)区分开——业务错误返回明确的 4xx,让客户端知道"是请求的问题,可以修正后重试"。
@app.errorhandler(404) def api_not_found(e): return {"error": "资源不存在", "status": 404}, 404 @app.errorhandler(500) def api_internal(e): # 生产环境记录日志,但返回通用错误(不泄露细节) app.logger.error(f"服务器错误: {e}") return {"error": "服务器内部错误"}, 500
API 错误设计原则:
⚠️ 常见坑:生产环境 debug 没关导致 500 页泄露源码。生产必须
debug=False,且用统一 500 处理器覆盖默认页面。
from flask import Flask, render_template_string, abort app = Flask(__name__) ERROR_404 = ''' <h1>404 - 页面不存在</h1> <p>你访问的页面可能已被移除。</p> <a href="/">返回首页</a> ''' ERROR_500 = ''' <h1>500 - 服务器出错了</h1> <p>请稍后再试,或联系管理员。</p> ''' @app.errorhandler(404) def not_found(e): return render_template_string(ERROR_404), 404 @app.errorhandler(500) def internal_error(e): return render_template_string(ERROR_500), 500 @app.route('/') def index(): return "首页,试试访问 /nonexistent 或 /crash" @app.route('/crash') def crash(): raise ValueError("故意出错") return "不会到这" if __name__ == '__main__': app.run(debug=True)
访问 /nonexistent(404 页)和 /crash(500 页)——友好错误页面生效。注意 debug=True 时 500 会显示调试页,关掉 debug 才走 errorhandler。
生产环境错误处理的重要一环是"记录错误":
import logging # 配置日志 logging.basicConfig(filename='app.log', level=logging.ERROR) @app.errorhandler(500) def internal_error(e): app.logger.error("服务器错误", exc_info=True) # 记录堆栈 return "服务器出错了", 500
日志的价值:用户看到友好页面,但后台记录完整错误(含堆栈)——"用户无感,开发可见"。生产环境配合监控告警(Sentry 等),错误出现即通知。
问:errorhandler 和 try/except 什么关系?
errorhandler 处理"HTTP 错误与未捕获异常";try/except 在视图内处理"预期内可恢复"的错误。能预期用 try/except,兜底用 errorhandler。
问:可以注册多个 500 处理器吗?
后注册的覆盖先注册的。通常一个应用一个 500 处理器就够。
问:蓝图可以有自己的 errorhandler 吗?
可以。@users_bp.errorhandler(404) 只对该蓝图内错误生效。全局错误用 @app.errorhandler。
问:怎么区分"4xx 客户端错误"和"5xx 服务器错误"?
4xx 是"你的请求有问题"(参数错、资源不存在),5xx 是"服务器有问题"(bug、依赖挂)。错误处理要让客户端能区分——4xx 提示修正请求,5xx 提示稍后重试。
错误处理让应用"出错了也不难看"。但"不同环境的配置怎么管理"还没解决。下一节看应用配置——开发/测试/生产环境的配置分离。
错误处理做得好不好,直接决定用户遇到问题时是"骂一句"还是"理解并重试"。工程化的错误处理有几个层面。
第一,用户看到的 vs 日志记录的。 对用户:友好、简洁、给出可行的下一步("返回首页""稍后重试");对开发:完整堆栈、请求参数、用户标识,方便复现。同一错误,两种呈现——这是专业应用与玩具应用的分水岭。实现方式:errorhandler 返回友好页面,同时用 app.logger.error 记录完整异常。
第二,错误码的语义化设计。 接口返回的错误信息应该"机器可读 + 人类可读":code(程序判断用,如 USER_NOT_FOUND)+ message(人看,如"用户不存在")+ details(可选,字段级错误)。前后端按这个约定联调,异常处理不再靠猜。这在 2.8 RESTful API 设计中会再次强调。
第三,异常的类型分层。 工程上常用"自定义业务异常":定义一个 ApiError(Exception),带 status_code 与 message 属性;业务代码里 raise ApiError(404, '订单不存在');全局 errorhandler 捕获它统一转 JSON。好处是业务代码里不用到处写 return xxx, 404,错误处理逻辑集中。
第四,404/500 之外别忘了这些。 403(无权限)、429(限流,请求太频繁)、413(上传太大)、502/503(上游/维护中)——不同状态码给用户不同的提示文案。Flask 默认对 4xx/5xx 都有默认页面,但内容很简陋,自定义 errorhandler 覆盖常见码是基本要求。
第五,错误处理与日志的配合。 生产环境永远看不到调试器,日志就是你的眼睛。建议:每次请求记录一条访问日志(方法、路径、状态码、耗时),5xx 记录完整堆栈。配合日志平台(ELK、云日志服务)做告警——5xx 比例突增时第一时间收到通知。这是"可观测性"的起点。
第六,开发期怎么"故意制造错误"来验证。 写好 errorhandler 后,临时在视图里 raise ValueError('test') 或访问不存在的 URL,确认错误页符合预期;再故意 raise HTTPException,确认状态码正确。错误处理代码也要被测试覆盖(第二章 2.6 测试里可以用 test_client 断言错误响应)。