HTTP QUERY 方法落地:用请求体承载复杂查询,又不放弃安全与缓存语义

2026-09-10 27 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

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、反向代理和应用缓存可能仍然只认识 GETHEAD,也可能使用“方法加 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

  • 创建订单、发送消息或触发付款;
  • 启动作业并在服务器上持久化任务状态;
  • 每次调用都会增加计数、消耗额度或产生不可重复副作用的动作。

采用前可以按这份清单推进:

  1. 在测试环境检查 SDK、浏览器调用方式、服务框架和完整代理链路。
  2. 明确请求体的 JSON Schema、大小上限和错误响应格式。
  3. 通过集成测试确认重复请求不会改变业务状态。
  4. 在开启共享缓存前,验证缓存键包含所有会影响结果的输入。
  5. 为暂不支持 QUERY 的客户端保留兼容入口,例如短期维持 POST /search,并明确其迁移周期。
  6. 监控 405501、WAF 拦截和缓存命中异常,而不是只观察应用层成功率。

QUERY 让 HTTP API 可以更准确地表达“带复杂请求体的读取操作”。真正的落地难点不在控制器里增加一个方法名,而在于让客户端、代理、缓存和安全设备对这套新语义达成一致。先从内部查询接口和可控链路试点,再逐步扩大使用范围,会比一次性替换所有搜索端点稳妥得多。


相关推荐