我们知道 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 架构风格 的重要组成部分。 下面是本文中涉及的几个状态码:

  1. 200 OK。对于GET,应当返回被请求资源的实体;对于POST,应当返回操作的结果。
  2. 302 Found。被请求的资源暂时位于另一个URI处,并且对于非HEAD/GET请求,用户代理在重定向前必须询问用户确认。RFC 1945 和 RFC 2068 规定客户端不允许更改请求的方法。但很多浏览器会将 302 当做 303 来处理(以 GET 方法重新发起请求)。
  3. 303 See Other。被请求的资源暂时位于另一个URI处,并且应当以GET方法去请求那个资源。
  4. 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 会非常令人恼火,考虑下面的场景:

  1. 地址栏键入 https://m.baidu.com/ss?word=harttle 并回车。这里多写了一个 s,我期望百度返回 404 并给我一次改正的机会。结果重定向后地址栏直接变成了 https://m.baidu.com/error.jsp,前面的 word 白敲了,而且 302 不产生历史记录,无法通过返回按钮来回到我拼写的 URL。
  2. 我想知道某个链接的 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。如有疏漏、谬误、侵权请通过评论或 邮件 指出。