IETF 发布 RFC 10008,正式给 HTTP 增加了一个新方法:QUERY。它瞄准的是一个长期尴尬的问题:很多查询操作本质上像 GET 一样安全、幂等,不应该修改服务器状态,但查询条件又复杂到不适合塞进 URL。过去大家只能在“超长 GET”和“语义不准的 POST”之间二选一,现在多了一个更贴切的选择。
它不是“带 Body 的 GET”,但确实解决了 GET 的痛点
GET 的优势很清楚:语义简单、安全、可缓存、容易被代理和浏览器理解。但它的查询参数通常放在 URL 里,现实中会遇到几个问题:
- URL 长度限制:浏览器、网关、代理、日志系统都可能有自己的上限。
- 结构表达别扭:复杂过滤条件、嵌套对象、数组组合用 query string 会很难读。
- 敏感信息外泄:URL 常被日志、Referer、监控系统记录,复杂查询条件不一定适合暴露在那里。
- 语义折中:很多团队用
POST /search表达查询,但POST默认不是安全方法,缓存和中间件也不会自然按查询请求处理。
QUERY 的定位是:像 GET 一样用于安全、幂等的检索,但允许请求体承载查询表达式。它不是让所有 GET 都迁移过去,而是给“复杂查询”一个更准确的 HTTP 语义。
语义重点:安全、幂等、可表达复杂查询
理解 QUERY 时,可以抓住三个关键词。
安全:客户端发送 QUERY 不应该触发业务状态变化。比如搜索商品、筛选日志、查询报表都适合;下单、扣库存、提交审批不适合。
幂等:同一个 QUERY 请求执行一次或多次,业务结果不应因为执行次数而改变。当然,底层数据可能随时间变化,这和 GET 一样,不等于响应永远相同。
请求体:复杂查询条件可以放进 body,例如 JSON、SQL-like DSL、Elasticsearch 风格查询、GraphQL-like 查询对象等。这样既避免 URL 过长,也让 API 描述更清晰。
一个典型请求可以长这样:
QUERY /orders/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"status": ["paid", "shipped"],
"created_after": "2025-01-01T00:00:00Z",
"customer": {
"country": "CN",
"vip": true
},
"sort": [{ "field": "created_at", "direction": "desc" }],
"limit": 20
}
这比把所有条件编码成 ?status=paid&status=shipped&customer.country=CN... 更稳,也比用 POST /orders/search 更能表达“我只是在查”。
可以这样实践:在 Node.js 里接收 QUERY
现实问题是:RFC 发布不代表所有框架、网关、CDN、浏览器立刻支持。落地时建议先从服务端和内部 API 开始试点,并保留兼容方案。
下面是一个最小 Node.js 示例,用原生 http 模块接收 QUERY 请求。保存为 server.js 后即可运行。
const http = require("http");
function readBody(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", chunk => {
body += chunk;
if (body.length > 1024 * 1024) {
req.destroy();
reject(new Error("request body too large"));
}
});
req.on("end", () => resolve(body));
req.on("error", reject);
});
}
const server = http.createServer(async (req, res) => {
if (req.method !== "QUERY" || req.url !== "/orders/search") {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "not found" }));
return;
}
try {
const rawBody = await readBody(req);
const query = rawBody ? JSON.parse(rawBody) : {};
const result = {
method: req.method,
received: query,
data: [
{ id: "ord_1001", status: "paid", total: 199 },
{ id: "ord_1002", status: "shipped", total: 299 }
]
};
res.writeHead(200, {
"Content-Type": "application/json",
"Cache-Control": "private, max-age=30"
});
res.end(JSON.stringify(result, null, 2));
} catch (error) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: error.message }));
}
});
server.listen(3000, () => {
console.log("listening on http://localhost:3000");
});
运行和调用:
node server.js
curl -X QUERY 'http://localhost:3000/orders/search' \
-H 'Content-Type: application/json' \
-d '{
"status": ["paid", "shipped"],
"created_after": "2025-01-01T00:00:00Z",
"limit": 20
}'
如果你的 HTTP 客户端、代理或 API 网关暂时不接受自定义/新方法,可以先设计一个降级入口:
curl -X POST 'http://localhost:3000/orders/search' \
-H 'Content-Type: application/json' \
-H 'X-HTTP-Method-Override: QUERY' \
-d '{"status":["paid"],"limit":20}'
这不是最终形态,但能帮助团队在协议生态完全跟上之前先统一 API 语义。
采用时要检查这些边界
QUERY 很有用,但不是无脑替换 GET 或 POST。
- 简单资源读取继续用
GET:例如GET /users/123没必要改。 - 会改变状态的操作继续用
POST、PUT、PATCH、DELETE:不要因为 body 方便就滥用QUERY。 - 检查网关和中间件:负载均衡、WAF、API Gateway、日志采集、SDK 生成器可能还不认识
QUERY。 - 明确缓存策略:虽然语义上适合查询,但实际缓存需要服务端、中间件、客户端共同支持。
- 控制 body 大小:复杂查询不等于无限查询,仍要限制请求体、分页、排序字段和过滤复杂度。
我的建议:先用在“复杂只读查询”上
最适合试点 QUERY 的地方,是搜索、报表、审计日志、订单筛选、数据分析这类接口:它们只读、条件复杂、经常超过 URL 舒适区,又不该被建模成会改变状态的 POST。
落地路径可以很务实:新内部 API 优先支持 QUERY;外部 API 同时提供 POST + X-HTTP-Method-Override 兼容;网关、监控、SDK 都确认支持后,再把它作为正式公开契约。QUERY 的价值不在于制造一个新潮动词,而是让 HTTP API 的语义终于少一点别扭。