默认 404 只有 207 字节,是给开发者看的:一句 NotFound 加一句拼写提示。 返回值后面那个 404 不能丢,否则页面内容对了、状态码却是 200。 500 的处理器只在 PROPAGATE_EXCEPTIONS 为假时才走——开发模式看不到它。
这一篇对应教材 10.1 到 10.3:建立自订错误页面的端点、模板,以及确认显示内容。 第 9 章把功能做齐了,这一篇管「出错的时候用户看到什么」。
一、默认的 404 是什么样
实测 207 字节,正文就五行:doctype、html、title、h1、p。
内容是 404 Not Found、Not Found、
The requested URL was not found on the server. 再加一句拼写提示。
它没有错:状态码对、信息也对。问题是它是给开发者看的,不是给用户看的。
普通用户看到一屏英文,第一反应是「这网站挂了」,第二反应是关掉。他不会去想「是不是我地址打错了」。
二、处理器怎么写
@app.errorhandler(404)
def not_found(e):
return render_template("errors/error.html", code=404,
title="页面不存在",
hint="地址是 " + request.path), 404
装饰器里的 404 是「哪个状态码触发这个函数」,返回值后面那个 404 是「真正发给浏览器的状态码」。
两个都要有。
实测自订页 103 字节,正文 <h2>404 · 页面不存在</h2>、<p>地址是 /nope</p>、一行回列表的链接。
只写 return render_template(...) 会怎样?页面内容对,状态码变成 200。
搜索引擎会把不存在的地址当成正常页面收录,监控也发现不了异常。
这个错很隐蔽:你在浏览器里看,页面长得完全对。要看状态码才知道。
三、把出错的地址带出来
| 请求 | 结果 | 页面上的地址 |
|---|---|---|
/nope |
404 | /nope |
/images/nope |
404 | /images/nope |
/images/file/nope.png |
404 | /images/file/nope.png |
三个地址都走同一个处理器,request.path 原样带出来。
这一点比默认页强得多:用户能截图给别人看「我点的是这个链接」,而不是只说「打不开」。
注意第三条:没登入的时候访问它拿到的是 302 不是 404——@login_required 比错误处理器先跑。
顺序这件事要记牢。
四、403 和 500
403 是 abort(403) 触发的。
实测用户 1 去检测用户 2 的图,页面显示「403 · 没有权限 / 这个资源不属于你」。
500 要靠异常触发。实测那个 1/0 的路由,页面显示「500 · 服务器出错了 / 已经记下来了,稍后再试」。
但 500 这个处理器有个前提:PROPAGATE_EXCEPTIONS 必须是假。
开发模式下(TESTING 或 DEBUG)异常会直接抛出来,因为那时候你要看堆栈。 所以你在本地测这个页面,怎么测都看不到——会以为处理器没注册上。 生产环境才需要它:用户看到一句「已经记下来了」,堆栈写进日志。 把堆栈直接给用户看,等于把源码结构、路径、库版本一起送出去。
五、处理器自己也会崩
errorhandler 函数就是一段普通代码:模板名可能打错、模板里可能用了不存在的变量、
request 在某些情况下可能拿不到东西。
它崩了会怎样?Flask 退回自己的兜底页。
不会无限递归,但也看不到你的自定义页——用户又回到那一屏英文。
所以自订错误页也要测:访问一个不存在的地址,确认页面上是你的文案,不是 Flask 那句 NotFound。
这条建议听起来很傻,但真有人部署完才发现错误页从来没生效过——因为模板路径写错了, 而平时没人访问不存在的地址。
六、错误页里该放什么
| 放 | 不放 |
|---|---|
| 状态码与一句话解释 | 堆栈、异常类名、文件路径 |
| 出错的地址 | 数据库连接串、密钥 |
| 一个回首页的链接 | 「请联系管理员」这种空话 |
| 一句「已经记下来了」 | 内部工单号、服务器名 |
左右两列其实是一件事的两面:左边是「用户能拿它做什么」,右边是「会让攻击者知道什么」。
第三行那对值得单独说:一个是路,一个是废话。用户要的是「下一步怎么办」。
还有一条没写进表里的:错误页别查数据库、别依赖 current_user 的业务字段。
数据库挂了的时候,错误页还得能渲染出来——那正是它最该出现的时刻。
七、分不清就一律说没有
| 操作 | 结果 |
|---|---|
/images/file/../../app.db |
404 |
| 删一个不存在的图 | 404 |
| 删别人的图 | 403 |
前两个是「这东西不存在」,第三个是「存在,但不给你」。
分不清「没有」和「不给」的时候,一律说「没有」。
这跟第 7 章 send_from_directory 的做法是一致的:越界请求返回 404,连文件在不在都不说。
站内给 403 是有好处的——用户知道是自己权限不够,不是链接坏了。对外接口就一律 404。
八、到这一章为止
第 2 篇到这里真的收尾了:能上传、能检测、能搜、能删,出错也有个像样的页面。 但「我觉得它能用」和「它确实能用」是两回事。下一篇换个角度:写测试,让机器来证明它没坏。 下一篇的每一行测试,测的都是前面九章做出来的东西。
九、动手 Demo:出错页:从一屏英文到一句话
这个 Demo 先看默认 404 长什么样,再注册自己的处理器,把 404、403、500 三种都试一遍。 第四节那个 500 有个坑:开发模式下根本测不到它。
def register(app):
@app.errorhandler(403)
def forbidden(e):
return render_template("errors/error.html", code=403,
title="没有权限",
hint="这个资源不属于你。"), 403
@app.errorhandler(404)
def not_found(e):
return render_template("errors/error.html", code=404,
title="页面不存在",
hint="地址是 " + request.path), 404
@app.errorhandler(500)
def server_error(e):
return render_template("errors/error.html", code=500,
title="服务器出错了",
hint="已经记下来了,稍后再试。"), 500
实跑输出:
--------------------------------------------------------------------
出错页:从一屏英文到一句话
--------------------------------------------------------------------
① 默认的 404 长什么样
--------------------------------------------------------------------
GET /nope -> 404,207 字节
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please
! 这就是 Flask 自带的 404。它没有错——状态码对、信息也对,只是给用户看的。
! 它给的是开发者看的内容:一句 NotFound、一段 Werkzeug 的版本号。
普通用户看到这个只会以为网站挂了。
② 注册自己的 404 处理器
--------------------------------------------------------------------
[代码原样]
| @app.errorhandler(404)
| def not_found(e):
| return render_template("errors/error.html", code=404,
| title="页面不存在",
| hint="地址是 " + request.path), 404
装饰器里的数字是状态码,返回值后面那个 404 是真正发给浏览器的状态码。
GET /nope -> 404,103 字节
<h2>404 · 页面不存在</h2>
<p>地址是 /nope</p>
<p><a href="/images/">回到图片列表</a></p>
! 注意返回值后面那个 404。
只写 return render_template(...) 的话,页面内容是自定义的,
状态码却是 200——搜索引擎会把不存在的页面收录进去。
③ 三种 404 都试一遍
--------------------------------------------------------------------
GET /nope -> 404 <p>地址是 /nope</p>
GET /images/nope -> 404 <p>地址是 /images/nope</p>
GET /images/file/nope.png -> 404 <p>地址是 /images/file/nope.png</p>
! 三个地址都走同一个处理器。
request.path 会原样带出来,所以用户能截图给别人看「我点的是这个链接」。
! 这一点比默认页强得多:默认页只说找不到,不说找不到什么。
! 第三个地址如果没登入,拿到的是 302 而不是 404——@login_required 比错误处理器先跑。
顺序这件事要记牢。
④ 403 与 500
--------------------------------------------------------------------
用户 1 检测用户 2 的图 -> 403
<h2>403 · 没有权限</h2>
<p>这个资源不属于你。</p>
<p><a href="/images/">回到图片列表</a></p>
GET /boom(除零)-> 500
<h2>500 · 服务器出错了</h2>
<p>已经记下来了,稍后再试。</p>
<p><a href="/images/">回到图片列表</a></p>
! 500 这个处理器只有 PROPAGATE_EXCEPTIONS=False 时才走。
开发模式下(TESTING 或 DEBUG)异常会直接抛出来,因为那时候你要看堆栈。
! 生产环境才需要它:用户看到一句「已经记下来了」,堆栈写进日志。
把堆栈直接给用户看,等于把源码结构、路径、库版本一起送出去。
⑤ 处理器里再出错会怎样
--------------------------------------------------------------------
! 这个处理器本身也可能出错:模板名打错、模板里用了不存在的变量、request 拿不到东西。
它是一段普通代码,一样会崩。
! 处理器里出错,Flask 会退回它自己的兜底页。
不会无限递归,但也看不到你的自定义页——用户又回到那一屏英文。
! 所以自订错误页也要测:访问一个不存在的地址,确认页面上是你的文案,不是 Flask 那句 NotFound。
⑥ 错误页该放什么
--------------------------------------------------------------------
| 放 | 不放 |
|---|---|
| 状态码与一句话解释 | 堆栈、异常类名、文件路径 |
| 出错的地址 | 数据库连接串、密钥 |
| 一个回首页的链接 | 「请联系管理员」这种空话 |
| 一句「已经记下来了」 | 内部工单号、服务器名 |
! 第三行那两个都是实话:一个是路,一个是废话。用户要的是「下一步怎么办」。
! 错误页不放导航和登录状态也行——它得在数据库挂了的时候也能渲染出来。
所以模板里别查库、别依赖 current_user 的业务字段。
⑦ 404 与 403 的顺序
--------------------------------------------------------------------
拿一个越界路径去取图 -> 404
删一个不存在的图 -> 404
! 两个都是 404,不是 403。分不清「没有」和「不给」的时候,一律说「没有」。
这跟第 7 章 send_from_directory 的做法是一致的。
! 顺序上还有一件事:@login_required 比 errorhandler 先跑。
没登入时访问任何页面都是 302 跳首页,走不到 403。
第一节 207 字节和第二节 103 字节,数字小但差别在「给谁看」。 第三节三条路径,第三条是个容易踩的顺序问题。 第四节那个 500 是靠把 PROPAGATE_EXCEPTIONS 关掉才看到的。
30 秒实验
- 把 404 处理器返回值后面那个 404 删掉,用浏览器开发者工具看响应码。
- 在 500 的处理器里写一句
1/0,再访问 /boom,看会显示什么。 - 想一想:为什么错误页里不该查数据库?
常见误区
- 返回值不写状态码:页面对了,状态码是 200,搜索引擎会收录。
- 指望开发模式能看到 500 页面:异常会直接抛出来,处理器不走。
- 错误页里显示堆栈:等于把源码结构和库版本送出去。
- 错误页里查数据库:数据库挂了的时候它自己也挂了。
自测三问(点开对答案)
1. 装饰器里的 404 和返回值后面的 404 分别是什么? —— 前者是「哪个状态码触发」,后者是「发给浏览器的状态码」。 2. 为什么开发模式下看不到自订 500 页? —— TESTING 或 DEBUG 时异常会直接抛出,PROPAGATE_EXCEPTIONS 为真,处理器不走。 3. 分不清 404 和 403 时怎么办? —— 一律 404。不告诉对方这东西存不存在。第 10 章到此结束。下一篇写测试:让机器证明这九章做的东西没坏。
- ☐ (本篇完,下一篇 11.1 pytest 入门:测试、断言与失败信息)