
先不聊标准、不聊 RFC,咱从你手头真会遇到的活儿说起。
假设你在做一个电商后台,运营天天来找你:「帮我查一下上个月,用了优惠券、金额大于 500、状态是已退款、收货地址在江浙沪的订单。」
条件五六个起步,而且随时加。你写接口,第一反应是 GET,因为「查数据用 GET」嘛:
GET /orders?coupon=true&minAmount=500&status=refunded®ion=YZH&start=2026-06-01&end=2026-06-30
一开始还好。可运营的需求越来越刁——「再加个按商品分类」「地址要支持多选十几个城市」「金额要区间还要排除某几档」。条件一多,这条 URL 就越拼越长,长到某天你会突然收到一个 414 URI Too Long 的报错。
这不是你写得烂,是 URL 本来就有长度上限,很多服务器和网关卡在 8000 字符左右。GET 把所有条件都挂在地址栏上,条件一复杂,地址栏就爆了。
这是 GET 的第一个业务级痛点:复杂筛选装不下。
还是这个订单接口。某天安全团队来找你喝茶:「你这查询接口,把用户手机号、邮箱都拼在 URL 里,全进了 Nginx 访问日志,还同步到了日志分析平台——这属于敏感信息泄漏。」
你一想,还真是。URL 会被原封不动记进各种日志:网关日志、CDN 日志、浏览器历史、甚至转发时的 Referer。你以为只是查个数据,结果查询条件里的用户隐私一路裸奔。
这是 GET 的第二个业务级痛点:参数全在明面上,藏不住。
被上面俩问题逼急了,你大概率会想:那我把条件塞进 POST 的 body 里不就完了——body 想多长多长,也不进 URL 日志。
POST /orders/search
{ "coupon": true, "minAmount": 500, "status": "refunded", ... }
看着是解决了。但你有没有遇到过这种诡异的线上问题:
用户网络卡了一下,前端一个查询请求转圈半天没回来,用户手一抖点了刷新/重试。结果呢,有时候好好的,有时候莫名其妙出问题。
根子就在 POST 身上。POST 这个动词,语义上是「我要改数据了」——新建订单、扣库存、发短信,都是 POST。它天生不保证「重发一次没事」。
打个比方你就懂了:
GET 像按电梯的「上行」键——你按 1 下和连按 10 下,电梯就来那一趟,多按不会多来九趟。重复了也无所谓。 POST 像餐厅点单——你说「来份宫保鸡丁」,说 1 次厨房做 1 份,网络抖了你重说一次,厨房可能真给你做 2 份。
所以你明明只是「查订单」(查 10 次也该是同一批数据),却用了「点单」这个动词。中间件、CDN、浏览器一看是 POST,全都不敢自作主张地帮你重试或缓存——因为它们分不清你这个 POST 到底改没改数据。
于是每个团队都在自己打补丁:加个幂等键、建张去重表、搞一套「这个 POST 其实是只读的,放心」的土规矩。本质上是在用一个语义错误的动词,硬凑一个只读查询。
一句话总结这十几年的窘境:
GET → 语义对(只读、可重试),但地址栏太窄,装不下复杂条件
POST → 身子够宽(能带 body),但语义错(像在改数据),不敢重试、不敢缓存
两个都不完美。我们就在这俩之间来回将就,将就了 16 年。
2026 年 6 月,官方(IETF)正式给 HTTP 加了个新动词 QUERY(标准编号 RFC 10008)。它干的事一句话就能说清:
它是「能带 body 的 GET」。
既像 GET 一样安全(只读、不改数据)、可以放心重试、可以被缓存;又像 POST 一样能把复杂条件放进 body 里。回头看前面那个订单接口,用 QUERY 就变成:
QUERY /orders
{
"coupon": true,
"minAmount": 500,
"status": "refunded",
"regions": ["杭州", "苏州", "上海", ...], // 想多复杂多复杂
"categories": ["数码", "家居"]
}
三个动词摆一起对比,谁能干啥一目了然:
GET POST QUERY
──────── ──────── ────────
能带 body ✗ ✓ ✓
只读/可放心重试 ✓ ✗ ✓
能被缓存加速 ✓ ✗ ✓
条件长度限制 受限 无 无
语义 查 改 查
▲ ▲
地址栏太窄 两边优点各拿一半
最右边这列,格格都是你想要的答案。运营再刁钻的筛选条件,塞 body 里;安全团队担心的隐私,不进 URL 日志了;用户手抖重试,因为它是「只读、幂等」的,重发一百次都是同一批数据,谁都不慌。
QUERY 还有一个特别适合业务的隐藏能力,很多人会错过。先看你天天在做的事——
老板要一张「实时经营大盘」,一堆筛选条件拼出来的复杂查询,他每天早上要刷一遍看最新数字。
按老办法(POST),服务器查完给你的是「这一次结果的地址」,比如 /results/xyz789。它是一张快照,指向的是你今早 9 点查出来的那批死数据。明天想看新的?对不起,你得把那一大坨查询条件重新拼一遍、重新发一次。
QUERY 反过来,它能给你「这次查询本身的地址」:
QUERY /orders → 服务器返回一个地址:/saved-queries/42
/saved-queries/42 存的不是结果,是你这条查询语句本身。所以以后你只要:
GET /saved-queries/42
# 服务器拿着原来那条查询,重新跑一遍,给你「此刻」的最新数据
体会一下这个差别:一个存的是「昨天查出来的东西」,一个存的是「这个问题本身」。落到业务上,这意味着——
把「一次复杂查询」变成一个能收藏、能分享、能定时重跑的永久链接——这对做后台、做报表、做监控的团队,省的可不是一点事。
再说个架构上的好处。你现在的用户接口,是不是长这样:
GET /api/users/active 查活跃用户
GET /api/users/inactive 查沉默用户
GET /api/users/premium 查付费用户
GET /api/users/new 查新注册
...
每来一种筛选口径,就得新开一个端点。时间一长,路由表几十上百个,改一处逻辑得改好几个地方,权限和监控也散得到处都是。
有了 QUERY,这些能收成一个:
QUERY /api/users
{ "status": "premium", "registeredAfter": "2026-01-01" }
所有筛选逻辑挪进 body,一个端点应对所有查询变体。要维护的路由少了,鉴权集中在一处,监控也好做——这是比「省几个字符」值钱得多的收益。
讲了这么多好处,得把丑话撂前头:这玩意儿现在还不能直接用到线上。
标准是 2026 年 6 月才发布的,太新了。真正认得 QUERY 这个动词的服务器、网关、CDN 还没几个——你线上一堆中间件可能压根不认它,请求发过去直接被拦掉。业内普遍估计,要等它真正好用、生态跟上,大概得到 2027 到 2028 年。
安全上也还有新坑要填。QUERY 允许「带 body 的只读查询」还能被缓存,这就多了些新的攻击面,比如把带敏感条件的查询缓存进了共享 CDN、或者老中间件对这个新动词处理有漏洞。配套的防护经验也还在积累。
所以现在的正确姿势是:先把它的思路吃透,做架构设计时把它记在心里;线上该用你那套成熟的 GET/POST 还是照旧。 等主流框架和网关都跟上了,再平滑切过去也不迟。
真好奇的话,本地玩一把很简单。Node 原生的 http 模块其实不挑方法名,你自己就能收一个 QUERY:
const http = require('http');
http.createServer((req, res) => {
if (req.method === 'QUERY') {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', () => {
const cond = JSON.parse(body || '{}'); // body 里就是你的筛选条件
const rows = runQuery(cond); // 换成你自己的查询逻辑
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(rows));
});
} else {
res.writeHead(405).end('Method Not Allowed');
}
}).listen(3000);
跑起来,你就能亲手把一段复杂条件用 QUERY 发出去、收回来,对这个新动词的感觉会具体很多。注意这只是本地玩具级验证,真正的缓存、幂等还得靠成熟中间件去落实。
QUERY 不是什么惊天动地的大招,它就是把 HTTP 缺了 16 年的一块拼图补上了:一个只读、能放心重试、能被缓存、还能带复杂 body 的查询动词。
它解决的,全是你写业务时真会撞上的老问题——筛选条件装不下、隐私进日志、用户手抖重试出 bug、报表要天天重发、接口越开越多。改动很小,但每一刀都砍在痛处。