📘 第四章 返回结果的HTTP状态码
📄 一句话概括

303 干脆规定死:用 GET 去 Location,不带原请求体。 它存在的唯一目的就是实现 PRG(Post/Redirect/Get)。

📝 本节和前面几节的关系

上一节说 302 没规定方法。 这一节看标准怎么补的——不是改 302,而是新造一个码,保证老客户端行为不变。

一、PRG 模式

POST 处理完不要直接返回结果页,而是回 303 指向结果页。 浏览器用 GET 去取那个页面,用户看到的是 GET 的结果。 这样刷新就变成了「重新 GET 结果页」,不会重发 POST。 这就是 PRG(Post/Redirect/Get),Web 表单的标配。 Demo 里订单接口就是这套:POST /order → 303 → GET /result/42。 服务端日志里两次请求方法清清楚楚。

二、为什么不能靠 302 实现

因为 302 不保证方法变成 GET。 如果某个客户端保持 POST 跟过去,/result/42 就会收到一个 POST——服务端要么不认(405), 要么把它当写操作处理(灾难)。 303 把这件事写死了:客户端必须用 GET。 所以服务端可以放心假设/result/42 只处理 GET,缓存也可以放心缓存它。 顺带一个细节:303 的响应正文通常为空,但可以带一段说明文字供老客户端显示。

三、303 与 307 是一对

这两个码是 1999 年一起补进 HTTP/1.1 的,正好把 302 的歧义劈成两半: 303 = 换成 GET(用于表单后的跳转);307 = 保持原方法(用于接口迁移)。 记住这一对,302 就没有存在的必要了。 这也是为什么现代框架(Django、Rails、Spring)的重定向默认行为都尽量靠近 303/307。

四、动手 Demo:订单提交:POST 进来,GET 出去

服务端收到 POST 就回 303,客户端按 303 的要求用 GET 去 Location,把两次请求行和正文长度都打出来。

if first.startswith(u'POST'):
    return (u'HTTP/1.1 303 See Other',
            [(u'Location', u'/result/42')], b'')
# 303 要求:用 GET 去 Location
s.sendall(_req(u'GET', hd.get(u'location')))

实跑输出:

--------------------------------------------------------------------
303 See Other:POST 之后去看结果
--------------------------------------------------------------------
  第 1 次请求                POST /order HTTP/1.1(正文 6 字节)
  第 2 次请求                GET /result/42 HTTP/1.1(正文 0 字节)

  303 强制要求               用 **GET** 去 Location,不带原请求体
  这就是 PRG                 Post/Redirect/Get,防重复提交的标准套路

  PRG 解决的是「刷新会不会重复下单」:
  POST 处理完回 303,浏览器跳到结果页(GET),
  用户刷新的是那个 GET 页面,不会重发 POST。
  302 做不到这一点,因为它没规定换不换方法;303 明确规定了。

第 1 次请求 POST /order(正文 6 字节)、第 2 次请求 GET /result/42(正文 0 字节)——方法换了、 正文没了,这正是 303 要求的。 303 强制要求 用 GET 去 Location,不带原请求体。这一句是本节全部内容。 这就是 PRG Post/Redirect/Get,防重复提交的标准套路——记住这个名字,面试和排障都用得上。

30 秒实验

  1. 把 303 换成 302,重跑,看你的客户端(模拟浏览器)行为会不会变(本例不会,因为脚本写死了 GET)。
  2. 把 303 换成 307,看第二次请求会变成什么方法。
  3. 想一想:如果结果页需要登录,303 之后那个 GET 会带 Cookie 吗?(会)

常见误区

  1. 「303 是 302 的新名字」——是两个码,302 仍然存在,只是语义含糊。
  2. 「303 之后客户端可以选方法」——不行,必须 GET。
  3. 「PRG 只在表单场景有用」——任何「提交后展示结果」的流程都适用。
自测三问(点开对答案) 1. 303 对方法有什么硬性规定? —— 客户端必须用 GET 去 Location,不带原请求体。 2. PRG 解决什么问题? —— 用户刷新结果页时不会重新提交 POST,避免重复下单。 3. 303 和 307 分别对应什么场景? —— 303 换成 GET(表单后跳转);307 保持原方法(接口迁移)。

跳转讲了一半。下一节看一个特殊的 3XX:它根本不跳,只是说「没变」。

相关笔记