我们知道 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 架构风格 的重要组成部分。 下面是本文中涉及的几个状态码:
使得搜索引擎索引错误的页面内容。 爬虫是一种特殊的用户代理,通常用于搜索引擎。虽然 Google 声称他们 “pretty tolerent of mistakes”,但即使 301 和 302 的表现也有 很大差距 。 一般来讲爬虫对状态码的处理倾向于:
301 等价于 canonical url + meta refresh 。302 有更多的不确定性,因为确实重定向了,但又不是永久的。 此外,如果没有采用 404 状态码会让爬虫认为你的网站存在众多重复页面:因为本该 404 的 URL 都以 302、200 的方式返回了同样的页面内容。
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 会变成下面这些错误:
Unexpected token
错误。 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。 /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)被缓存了。 适用于这个例子的不只是脚本,还包括样式、图片、字体,即所有可缓存的资源都有问题。
也就是说由于错误地使用状态码,我们无法从错误中恢复,只能寄希望于用户主动清除浏览器缓存。