🧪 第10章 建立自定义错误页面
📄 一句话概括

默认 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 秒实验

  1. 把 404 处理器返回值后面那个 404 删掉,用浏览器开发者工具看响应码。
  2. 在 500 的处理器里写一句 1/0,再访问 /boom,看会显示什么。
  3. 想一想:为什么错误页里不该查数据库?

常见误区

  1. 返回值不写状态码:页面对了,状态码是 200,搜索引擎会收录。
  2. 指望开发模式能看到 500 页面:异常会直接抛出来,处理器不走。
  3. 错误页里显示堆栈:等于把源码结构和库版本送出去。
  4. 错误页里查数据库:数据库挂了的时候它自己也挂了。
自测三问(点开对答案) 1. 装饰器里的 404 和返回值后面的 404 分别是什么? —— 前者是「哪个状态码触发」,后者是「发给浏览器的状态码」。 2. 为什么开发模式下看不到自订 500 页? —— TESTING 或 DEBUG 时异常会直接抛出,PROPAGATE_EXCEPTIONS 为真,处理器不走。 3. 分不清 404 和 403 时怎么办? —— 一律 404。不告诉对方这东西存不存在。

第 10 章到此结束。下一篇写测试:让机器证明这九章做的东西没坏。

相关笔记