2026 年 6 月,IETF 发布 RFC 10008,为 HTTP 增加了 QUERY 方法。这是自 2010 年以来首个新的标准 HTTP 方法,目标是解决一个长期存在的 API 设计难题:复杂查询需要结构化请求体,但开发者又希望保留类似 GET 的安全、幂等和可缓存语义。
过去,团队通常只能在“塞满 URL 的 GET”和“语义并不准确的 POST”之间选择。QUERY 提供了更明确的第三条路,但标准发布并不意味着现有网关、框架和缓存会立即正确支持它。
GET 和 POST 之间缺少的那块拼图
考虑一个搜索接口,它需要表达多层筛选、排序和分页条件:
{
"filters": {
"status": ["paid", "shipped"],
"total": {"gte": 100, "lt": 1000},
"customer": {
"country": ["CN", "SG"]
}
},
"sort": [
{"field": "created_at", "direction": "desc"}
],
"page": {"size": 50, "cursor": "next-page-token"}
}
把这类结构编码进查询字符串,会带来可读性、长度限制、数组与嵌套对象编码规则不统一等问题。给 GET 附加请求体也不是稳妥方案,因为不少客户端、代理和服务器不会一致处理它。
改用 POST /orders/search 虽然容易实现,却模糊了请求语义。HTTP 生态通常把 POST 看作可能改变服务器状态的操作,缓存、中间件、重试策略和可观测性工具也会据此采取更保守的行为。
QUERY 的核心价值不是“允许请求体”这么简单,而是把几个属性组合到同一种方法中:
| 方法 | 通常使用请求体 | 安全语义 | 幂等语义 | 面向缓存的查询语义 |
|---|---|---|---|---|
GET |
不宜依赖 | 是 | 是 | 是 |
POST |
是 | 否 | 否 | 通常较弱 |
QUERY |
是 | 是 | 是 | 是 |
这里的“安全”并不等于认证安全或数据保密,而是指客户端没有要求服务器改变资源状态。服务器仍然可能记录日志、更新指标或填充缓存。“幂等”则表示重复执行同一个查询,不应产生不同的业务副作用。
可以这样实践:搭一个最小 QUERY 服务
下面的示例只依赖 Python 标准库,可以用来验证客户端和服务器是否能发送、接收 QUERY 请求。它是协议实验代码,不包含生产环境需要的认证、限流和共享缓存。
将以下内容保存为 server.py:
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
ORDERS = [
{"id": 1, "status": "paid", "total": 120},
{"id": 2, "status": "pending", "total": 80},
{"id": 3, "status": "shipped", "total": 450},
]
class Handler(BaseHTTPRequestHandler):
def do_QUERY(self):
if self.path != "/orders":
self.send_error(404)
return
try:
length = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(length) or b"{}")
except (ValueError, json.JSONDecodeError):
self.send_error(400, "Request body must be valid JSON")
return
filters = payload.get("filters", {})
statuses = set(filters.get("status", []))
minimum = filters.get("min_total", 0)
result = [
order
for order in ORDERS
if (not statuses or order["status"] in statuses)
and order["total"] >= minimum
]
body = json.dumps({"items": result}, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "public, max-age=60")
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
server = ThreadingHTTPServer(("127.0.0.1", 8000), Handler)
print("Listening on http://127.0.0.1:8000")
server.serve_forever()
启动服务:
python3 server.py
在另一个终端发送请求:
curl --request QUERY 'http://127.0.0.1:8000/orders' \
--header 'Content-Type: application/json' \
--data '{
"filters": {
"status": ["paid", "shipped"],
"min_total": 100
}
}'
预期响应如下:
{
"items": [
{"id": 1, "status": "paid", "total": 120},
{"id": 3, "status": "shipped", "total": 450}
]
}
如果框架没有提供专门的 QUERY 装饰器,通常可以从“自定义 HTTP 方法路由”入手。不过具体配置属于框架实现细节,上线前必须确认路由器不会把未知方法转换成 GET、拒绝为 405 Method Not Allowed,或者在读取请求体之前丢弃它。
可缓存不代表缓存会自动生效
RFC 赋予方法可缓存语义,只是让缓存成为可能。现实中的 CDN、反向代理和应用缓存可能仍然只认识 GET 与 HEAD,也可能使用“方法加 URL”作为缓存键,完全忽略请求体。
这对 QUERY 尤其危险。下面两个请求拥有相同 URL,但含义不同:
QUERY /orders HTTP/1.1
Content-Type: application/json
{"filters":{"status":["paid"]}}
QUERY /orders HTTP/1.1
Content-Type: application/json
{"filters":{"status":["cancelled"]}}
如果缓存键没有区分请求体,第二个请求可能收到第一个请求的结果。实践中至少要验证:
- CDN、API 网关、WAF 和反向代理是否允许
QUERY方法通过; - 缓存实现是否明确支持该方法;
- 缓存键是否考虑完整请求体及其媒体类型;
- JSON 字段顺序或空白不同但语义相同的请求,是否需要规范化;
- 授权身份、租户、语言和内容协商信息是否被正确隔离;
- 不可共享的用户数据是否被错误标记为公共缓存。
不要仅仅给响应加上 Cache-Control 就假定链路已经安全。缓存键设计错误会造成结果串用,严重时会演变为跨用户数据泄漏。
迁移时别把 QUERY 当成 POST 的批量替换
适合迁移到 QUERY 的端点通常具有三个特征:操作只读取数据、筛选表达式明显复杂、相同输入可以重复执行而不产生业务副作用。例如报表查询、商品检索、日志搜索和分析任务的同步查询接口。
以下操作则不应因为“请求体很复杂”就改成 QUERY:
- 创建订单、发送消息或触发付款;
- 启动作业并在服务器上持久化任务状态;
- 每次调用都会增加计数、消耗额度或产生不可重复副作用的动作。
采用前可以按这份清单推进:
- 在测试环境检查 SDK、浏览器调用方式、服务框架和完整代理链路。
- 明确请求体的 JSON Schema、大小上限和错误响应格式。
- 通过集成测试确认重复请求不会改变业务状态。
- 在开启共享缓存前,验证缓存键包含所有会影响结果的输入。
- 为暂不支持
QUERY的客户端保留兼容入口,例如短期维持POST /search,并明确其迁移周期。 - 监控
405、501、WAF 拦截和缓存命中异常,而不是只观察应用层成功率。
QUERY 让 HTTP API 可以更准确地表达“带复杂请求体的读取操作”。真正的落地难点不在控制器里增加一个方法名,而在于让客户端、代理、缓存和安全设备对这套新语义达成一致。先从内部查询接口和可控链路试点,再逐步扩大使用范围,会比一次性替换所有搜索端点稳妥得多。