地点筛选勾选太多导致网址过长,页面报 431 的排查记录
1825 字约 7 分钟
摘要Job Market Monitor 的岗位页在筛选工作地点时报 HTTP 431。原因是“全选后取消几项”会把几百个地点逐项写进网址,超出了 Web 服务约 16 KB 的限制。第一版修复改用 POST 请求体和紧凑网址,真实页面验证又暴露出慢查询,最后在我的追问下干脆取消了筛选分享。
Job Market Monitor 的岗位页可以按工作地点筛选。地点选择器分成国内城市、海外城市、国家或地区、其他地点几组,每组都有“全选”。有一天我在页面上筛选,列表突然加载失败,提示 API 请求失败,状态码 431。
我把出错时地址栏里的链接保存下来交给助手。链接很长,它先量了一下,约 34 KB。
633 个地点
助手用浏览器复现了问题。正常的岗位页返回 200,我给的长链接和对应的岗位接口都返回 431,而且不带登录 Cookie 也一样,所以可以先排除登录状态。
链接里一共有 633 个地点选项:上海,加上全部海外城市、全部国家或地区、全部其他地点,再加上“未明确”。国内城市分组里只剩上海,其他几组却仍然全部处于选中状态,这是“全选之后又取消了国内城市”才会留下的状态。
前端的做法是把“选中的每个地点”逐个写进网址,所以“全选后取消几项”会让网址里出现几百个编号。助手用不同数量的编号试了一下:
| 地点数量 | 请求目标大小 | 结果 |
|---|---|---|
| 300 | 约 16.2 KB | 200 |
| 310 | 约 16.7 KB | 431 |
| 935 | 约 50 KB | 431 |
935 是“从全部地点里只取消一项”得到的数字。也就是说,这不是我碰巧选了什么奇怪的组合,而是最普通的“全选再排除”就会触发。
通俗比喻:门卫只收一张便条请求像一张递给门卫的便条,网址写在便条最上面一行。门卫规定便条不能超过半页纸,超过了就直接退回,连内容都不看,这就是 431。
把几百个地点的名字都写在最上面那一行,自然会超。
第一版修复
助手一开始就提醒,只调大服务器的限制不能彻底解决,选项继续增加还会再失败。它给了一个三处同时改的方案,并且列出了容易引入新问题的地方:
- 不能截断。用户选了多少,就要准确查多少,不能只取前 100 项。
- 不能简单改成“排除未选地点”。一个岗位可能同时在上海和北京招聘,排除的写法会把本来符合条件的岗位漏掉。
- 简历匹配的接口当时还限制最多传 100 个地点,只修普通查询,匹配功能照样会失败。
- 已经保存下来的超长旧链接,在页面加载之前就被服务器拒绝了,页面里的代码救不了它,需要另外设计一个恢复入口。
实现上分成两部分。查询超过 1800 字节时,前端改用 POST,把完整的筛选条件放进请求体,权限、限流和缓存隔离仍走原来的查询逻辑。页面网址则用带版本号的紧凑格式,放在 # 后面,浏览器自己解析,不会发给服务器。这样刷新、复制链接、在新标签页打开,都能还原整套条件。
真实页面里的 20 秒
测试和构建都过了,部署之后助手又用浏览器打开我最初的那条长链接验收。请求不再是 431 了,但页面一直在转圈。助手之前用脚本直接请求后端接口时,也遇到过这组筛选 20 秒都没有返回。
这是第二个问题,之前被 431 挡住了,根本没机会暴露。查执行计划之后发现,为了按顺序返回前 20 条,数据库在大范围地点条件下反复遍历了大批岗位。助手把查询改成先按条件筛出岗位,再排序分页。同样的 633 项筛选,约 3.7 秒返回,得到 20,048 个岗位,原有的接口回归也都通过了。
验收时还有两个小发现。一是从“岗位方向”页跳转过来时,“没有选中任何项”可能被转成“不限”,也就是什么都没选反而查出了全部岗位,这次一起改了。二是手机上“导入筛选链接”和“清除全部”两个按钮挤在了一起。
一定要这么长的链接吗
到这里问题已经修好了。但地址栏里还留着一个 #filters=v1. 开头的长串,于是我问助手,这个筛选功能一定要靠这么长的链接才能做到吗?
助手的回答很坦率。筛选本身并不依赖长链接,大量条件已经通过 POST 请求体发送了。链接之所以还这么长,是因为我把完整条件也存进了网址,用来支持刷新恢复、前进后退和分享,而当初这么做,是为了在修复 431 的同时不引入服务端存储。它给的另一个方案是点击“分享”时把条件存到服务端,生成 /jobs?s=Ab3k9x 这样的短链接。
我更需要的其实是切换页面再切回来,筛选条件还在,分享一组筛选条件并不是刚需。所以我的决定是干脆不提供分享筛选。
于是第一版里的紧凑网址、# 后面的状态、“导入筛选链接”入口都被去掉了。筛选条件、搜索框里还没提交的文字、页码和每页条数,改为保存在当前标签页的浏览器存储里。验证下来:
- 切到别的页面再回来,或者刷新页面,筛选、搜索内容、页码和展开状态都能恢复,地址栏始终是干净的
/jobs。 - 从“岗位方向”页点“查看岗位”,仍然会带着对应的条件过来;浏览器后退则恢复之前的筛选。
- 935 项地点的选择完整保留,请求还是走 POST,没有长网址,也没有 431。
浏览器禁用存储时的一个旧问题改用浏览器存储之后,助手顺手做了异常场景检查,发现浏览器禁用存储时,一段清理旧缓存的代码会抛异常,打断权限的加载,页面会一直停在“正在确认访问权限”。这是以前就存在的问题,和这次改动无关,只是被这次检查带了出来。已经改成存储不可用时跳过清理,并补了回归测试。
回头看
这次修复走了一个弯路。第一版的方案没有错,它也确实通过了所有验证,但它解决的是“怎样把几百个地点塞进网址”,而没有先问“为什么这些东西要放在网址里”。网址承担的是三种职责:分享、刷新恢复、前进后退。其中分享用不上,刷新恢复和前进后退放在浏览器存储里就够了。
另外,431 其实只是第一个出现的问题。它后面藏着一个 20 秒的慢查询,要不是修完之后又拿原始链接在真实页面里验收一遍,这个慢查询会一直躲在 431 的后面。