状态码很重要
我们知道 HTTP 状态码 用来标识响应的状态,不恰当的状态码可能会影响 SEO,用户体验和可访问性,甚至产生不可恢复的线上问题。 因为状态码不仅仅是客户端 AJAX 的返回值,它对 Web 系统架构有着重要的影响。
但有些网站从不返回 4xx,用 3xx 或 200 来处理错误。可能是为了减少错误报警来提升 KPI(比如有些老板分不清 4xx 和 5xx),可能是为了减少 nginx 返回页面的大小(比如直接 302 到 CDN),也可能是 HTTP 时代 ISP 和路由器会劫持 4xx 打自己的广告(比如 如何看待小米路由进行 404 网页劫持?)。 我们不去细究原因,只把它作为案例来讨论 404/302 状态码的误用对 Web 系统的影响。
状态码及其语义
HTTP 是一种请求/响应协议,除客户端、服务器外还可能涉及代理、网关、隧道,响应状态码会影响各方的处理方式。 正如 R. T. Fielding 的论文中强调的,架构风格对系统的简单性和伸缩性都有重要的影响,而 HTTP 语义是 REST 架构风格 的重要组成部分。 下面是本文中涉及的几个状态码:
- 200 OK。对于GET,应当返回被请求资源的实体;对于POST,应当返回操作的结果。
- 302 Found。被请求的资源暂时位于另一个URI处,并且对于非HEAD/GET请求,用户代理在重定向前必须询问用户确认。RFC 1945 和 RFC 2068 规定客户端不允许更改请求的方法。但很多浏览器会将 302 当做 303 来处理(以 GET 方法重新发起请求)。
- 303 See Other。被请求的资源暂时位于另一个URI处,并且应当以GET方法去请求那个资源。
- 404 Not Found。服务器未能找到URI所标识的资源。也常被用于服务器希望隐藏请求被拒绝的具体原因。例如 403、401 可能会被统一处理为 404。
影响 SEO 和爬虫
使得搜索引擎索引错误的页面内容。 爬虫是一种特殊的用户代理,通常用于搜索引擎。虽然 Google 声称他们 "pretty tolerent of mistakes",但即使 301 和 302 的表现也有很大差距。 一般来讲爬虫对状态码的处理倾向于:
- 404。该页面不存在(死链),不对它进行索引。
- 301。URL 被站长永久地改掉了,索引重定向地址并把原页面的权重转移过去,被检索时展示后者。
- 200。页面成功获取,即使这个页面内容为“404 未找到”,也会被入库并在搜索结果中展现。
301 等价于 canonical url + meta refresh。302 有更多的不确定性,因为确实重定向了,但又不是永久的。 此外,如果没有采用 404 状态码会让爬虫认为你的网站存在众多重复页面:因为本该 404 的 URL 都以 302、200 的方式返回了同样的页面内容。
用户可见 Bug
HTTP 错误被当作成功处理,产生无法预料的效果。 3xx 和 4xx 对 AJAX/fetch 的区别在于是否被判定为发生了错误。 比如下面的代码片段功能是,获取用户的富文本个性签名,并显示到页面中:
const res = await fetch('/api/user/harttle/bio')
const el = document.querySelector('.bio')
el.html(res.text())
考虑发生 4xx 错误的情况。由于 302 的语义是“Found”,用 302 替代 404 上述请求会成功返回而不抛错。 错误页面的内容会被塞到 DOM 中,产生类似“俄罗斯套娃”的效果。 为此我们需要建设一套 AJAX/fetch 工具库:把 302 当作错误,为了使得其他场景也可以使用 302 状态码,还得只针对特定的 Location 响应头生效这一策略。 这又使得该工具库和 nginx 配置的 redirect 地址产生耦合。
调试信息变得不直观
404 Not Found 变成了 Unexpected token
。
302 替代 404 这件事情在浏览器看来,就是失败变成了成功。本来应该失败的过程会继续往后走,不再抛出 404 Not Found,而是抛出后续的具体处理异常,导致页面调试变得困难。分类来讲,本来的 404 Not Found 会变成下面这些错误:
- AJAX/fetch JSON 被重定向到错误页面时。HTML 内容会被当做 JSON 解析而产生一个
Unexpected token
错误。 - AJAX/iconfont 等资源被重定向到错误页面(通常是 CDN URL)时,线下域名调试时会发生跨域错误
Access-Control-Allow-Origin
。 - 脚本资源被重定向到错误页面时,会发生类似
unexpected token <
的解析错误。因为 HTML 文件第一个非空字符是<html>
中的<
,它不是合法的 JavaScript。 - 样式资源被重定向到错误页面时,会发生
Resource interpreted as Stylesheet but transferred with MIME type text/html
报警。
此外 Chrome Network 不会把 302 的资源标注为红色(因为 302 的语义不是错误),为了定位产生错误的资源,
你需要去 Chrome DevTools 的 Network 中搜索 status-code:302
。
网站的可访问变差
强制隐藏错误信息,使网站变得难以使用。 网站发生 404 错误时,通常是用户 URL 拼写有误,或点击了错误的链接。 此时返回 3xx 会非常令人恼火,考虑下面的场景:
- 地址栏键入
https://m.baidu.com/ss?word=harttle
并回车。这里多写了一个 s,我期望百度返回 404 并给我一次改正的机会。结果重定向后地址栏直接变成了https://m.baidu.com/error.jsp
,前面的 word 白敲了,而且 302 不产生历史记录,无法通过返回按钮来回到我拼写的 URL。 - 我想知道某个链接的 URL 所以点击了这个链接。期望从地址栏能够拷贝、编辑或收藏这个 URL。这个流程对失效的、返回 404 的链接也 OK,但如果它被 302 走了,我只能得到一个毫无意义的错误页的 URL,没法编辑或收藏,刷新(一个 URL 是
/error.html
的页面)也毫无意义。
细心的读者可能注意到了,“我想知道某个链接的 URL 所以点击了这个链接”很不专业也很不安全,大可以右键复制嘛。我们来个更好的例子: 比如我是从某个论坛网站/短网址服务上得到的链接,这个链接需要经过一次跳转才能到源站,那么现在它会自动跳转两次。 要想知道它指向的 URL 到底是什么我需要打开 Chrome Network 控制台或者手动 curl。
可是为什么非要查看一个失效的链接呢?因为我假设这个 URL 包含了有用信息(URL 不包含任何有用信息的情况可访问性是零,没法变差了),比如它的域名(这样我就可以去它的网站上搜索),它的路径(比如可能只是拼写错误,我可以纠正它并继续访问)。这些信息不再对用户可用,就意味着这种场景下网站的可访问性已经变差。
不可恢复的资源错误
误用 302 会导致无法恢复的资源错误。 我们说到 HTTP 涉及多方,涉及到客户端、代理、网关、服务器,HTTP 协议描述了哪些状态码是可缓存的。 比如 4xx 是禁止缓存的,而 302、200 是可缓存的。虽然浏览器不会缓存主文档,但静态资源仍然可以被缓存。 这意味着 302 替代 404 还有一个后果:如果浏览器访问过一个不存在的资源,该 302 会被缓存,即使文件已经存在了。直到用户清除缓存。
例如,我们在 HTML 中引用 JavaScript 文件。因流程错误导致 HTML 首先部署生效,用户访问页面时 JavaScript 被 302 导致功能异常。 即使我们尽快完成了 JavaScript 部署,用户重新访问或刷新页面并不会得到修复:错误的 JavaScript(内容为错误页的 HTML)被缓存了。 适用于这个例子的不只是脚本,还包括样式、图片、字体,即所有可缓存的资源都有问题。
也就是说由于错误地使用状态码,我们无法从错误中恢复,只能寄希望于用户主动清除浏览器缓存。
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2020/06/25/status-code-matters.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。