HTTP 迎来一个专为搜索设计的新方法
了解 HTTP QUERY:一种通过请求正文执行搜索的安全、幂等方法。查看缓存与重试机制、支持它的客户端,以及可能拦截请求的基础设施。
HTTP QUERY 方法由 2026 年 6 月发布的 RFC 10008 定义。它像 POST 一样把查询内容放在请求体中,又像 GET 一样具备安全性和幂等性。因此,QUERY 的响应可以被缓存,失败的请求也可以自动重试。
大多数 API 团队都见过这样的搜索接口:最初是一个简洁的 GET 请求,后来 URL 里塞满了经过 URL 编码的嵌套过滤条件、日期范围和排序字段。接着有人把它改成了 POST,缓存和重试从此失效。
本文介绍 QUERY 要解决的问题、QUERY 请求与响应的实际报文格式,以及技术栈中哪些组件已经支持它、哪些仍会拒绝它。
核心要点
- QUERY 是一个安全、幂等的 HTTP 方法,查询内容放在请求体中。它已在 RFC 10008(2026 年 6 月)中完成标准化。
- 如果 QUERY 请求缺少 Content-Type,或者 Content-Type 与请求体不符,服务器必须拒绝该请求。缓存 QUERY 响应时,缓存键必须包含请求体。
- 脚本可以通过
fetch()发送 QUERY,但设置了method="query"的 HTML 表单会回退为 GET,并丢弃请求体。 - 浏览器发起的跨域 QUERY 请求总会触发 CORS 预检,因为 QUERY 不属于 CORS 安全列表方法。
- 任何不认识该方法的服务器、代理或 WAF,都可能在请求到达应用之前将其拒绝。
为什么搜索接口总是在 GET 和 POST 之间左右为难?
对于复杂搜索,GET 和 POST 都不理想。GET 要把所有过滤条件放进 URL。POST 把过滤条件移到了请求体中,却会告诉所有中间节点:这个请求可能改变服务器状态。
RFC 9110 要求所有发送方和接收方至少能够处理 8,000 个八位字节长度的 URI。这只是下限,并不是上限。HTTP 本身没有规定 URI 的长度上限,所以链路上的任何代理、网关或服务器都可能拒绝更长的 URL。此外,过长的查询字符串还会留在浏览器历史记录、服务器日志和书签中,导致过滤条件里的内容泄露。
POST 可以避开这些问题,但它既不安全,也不幂等。幂等请求无论发送一次还是十次,预期效果都相同。RFC 9110 规定,除非客户端能通过某种方式确认请求在实际中是幂等的,否则不应自动重试非幂等请求。因此,缓存通常不会复用 POST 响应,网关也不会重试中途失败的 POST 请求。
什么是 HTTP QUERY 方法?
根据 RFC 10008,QUERY 请求会让服务器执行请求体中描述的查询并返回结果,同时不改变服务器上的任何状态。因此,客户端或代理可以放心地重发或重新发起 QUERY 请求,不必担心中途失败的首次尝试已经改变了状态。POST 则无法做出这样的假设。下面是这个请求的实际报文(各组成部分的说明请参阅 HTTP 请求剖析):
QUERY /products/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{"category": "laptops", "price": {"max": 1500}, "brands": ["acme", "globex"], "sort": "price_asc"}
查询由请求体及其媒体类型共同定义。如果缺少 Content-Type 头,或者它与请求体的实际内容不符,RFC 10008 要求服务器拒绝该请求。RFC 10008 的状态码指引建议返回 4xx 状态码:未提供媒体类型时返回 400 等状态码,资源不支持所发送的媒体类型时返回 415。
响应可能如下所示:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
Accept-Query: application/json
Content-Location: /products/search/results/7f3a
Location: /products/search/queries/7f3a
{"results": [ ... ]}
每个头部都有各自的用途:
- **缓存键:**QUERY 响应是可缓存的。缓存存储这类响应时,会把请求体纳入缓存键。原因是,向同一 URL 发送的两个 QUERY 请求如果请求体不同,查询的就是不同的内容。
- **
Accept-Query:**服务器通过这个响应头列出它接受的查询格式,格式为由媒体类型组成的结构化字段(Structured Field)列表。对于服务器上路径相同的所有 URI,无论查询字符串是什么,都共用同一个值。 - **Content-Location 与 Location:**在 QUERY 响应中,Content-Location 标识这次查询的结果。Location 则为查询本身提供一个 URI,客户端之后可以直接用普通 GET 请求获取它。这些头部的常规行为可参阅 HTTP 响应里有什么一文。
示例中的 Cache-Control 只是普通的缓存配置,RFC 10008 并不强制要求它。
QUERY 目前在哪些环境中可用?
只要客户端和服务器之间没有组件拒绝它,HTTP QUERY 方法已经可以在多个服务端运行时、框架和 HTTP 客户端中使用。
- Node.js 的 http 模块支持 QUERY。其内置解析器 llhttp 定义了
HTTP_QUERY(参见 Node 中的llhttp.h),并从 llhttp 9.2 起能够识别该方法。根据 Node.js 更新日志,这个解析器版本随 v21.7.2 发布,v22 及更高版本和 v20.19.2 也包含该版本。在这些版本中,http.METHODS包含'QUERY',服务端处理函数会看到req.method === 'QUERY'。 - Go 的 HTTP 客户端在
http.NewRequest中接受任何合法的方法标记作为方法字符串,所以现在就可以发送 QUERY:
package main
import (
"log"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"category":"laptops","price":{"max":1500}}`)
req, err := http.NewRequest("QUERY", "https://api.example.com/products/search", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
}
- Laravel:Laravel 13.19.0 为 QUERY 方法新增了
Http::query()客户端方法,以及query()/queryJson()测试辅助方法。路由可以通过Route::match()接受 QUERY,相关测试辅助方法由 laravel/framework PR #60662 引入。专用的Route::query()辅助方法已合并到 Laravel 的 master 分支,而不是 13.x 分支,因此目前没有任何 13.x 版本包含它。
在浏览器中,同源脚本可以通过 fetch() 发送 QUERY。跟踪 QUERY 支持情况的 Fetch issue(whatwg/fetch#1938)指出,Fetch 标准既没有禁止 QUERY,也不会对其做大小写规范化。浏览器会按原样发送你写的方法字符串,所以请始终使用大写:
const res = await fetch('/products/search', {
method: 'QUERY',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category: 'laptops', price: { max: 1500 } }),
});
const data = await res.json();
哪些环境尚不支持 QUERY?
目前,HTTP QUERY 方法面临三个障碍:HTML 表单无法发送 QUERY;部分服务器和安全层会直接拒绝该方法;跨域调用需要预检。
**表单。**脚本可以通过 fetch() 发送 QUERY,但表单不行。支持 method="query" 的提案是 WHATWG HTML issue #12594,目前仍未关闭,并被标记为 “needs implementer interest”(有待实现方表达兴趣)。在该提案落地之前,下面这段标记:
<form method="query" action="/products/search">
<input name="category" value="laptops">
<button>Search</button>
</form>
到达服务器时会变成一个丢弃了请求体的 GET 请求,因为浏览器会把无法识别的表单方法当作 GET 处理。为该 issue 制作的在线复现示例展示了这一行为。
**服务器、网关与 WAF。**任何不认识该方法的节点都可能拒绝它。同一个复现示例还提到,例如 LiteSpeed 会在应用运行之前就对 QUERY 返回 400。反向代理、负载均衡器、CDN、API 网关和防火墙都需要逐一检查。
**跨域预检。**QUERY 不在 Fetch 标准的 CORS 安全列表方法中。因此,无论携带什么请求头,跨域 QUERY 请求都会触发预检:
OPTIONS /products/search HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: QUERY
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, QUERY
Access-Control-Allow-Headers: content-type
如果 Access-Control-Allow-Methods 中没有列出 QUERY,浏览器会拦截实际请求。
| 环境 | 状态 |
|---|---|
fetch(),同源 | 可用 |
fetch(),跨域 | 预检允许 QUERY 后可用 |
| HTML 表单 | 回退为 GET,请求体被丢弃 |
| Node.js http | 可用(v20.19.2+、v21.7.2+、v22+;llhttp 9.2 可识别 QUERY) |
| Go HTTP 客户端 | 可用(自定义方法字符串) |
| Laravel 13.19+ | 可用(客户端、测试、Route::match) |
| LiteSpeed | 在应用运行前返回 400 拒绝(见 #12594 复现示例) |
| 代理、CDN、网关、WAF | 取决于具体产品及其配置 |
GraphQL 应该使用 QUERY 吗?
QUERY 适合 GraphQL 的查询(query)操作。这类操作只读取数据,与安全、幂等且携带请求体的方法相契合。变更(mutation)操作会改变状态,因此仍应使用 POST。如果你正在权衡这两种 API 风格,可以参阅 GraphQL 与 REST 详解,了解两者的取舍。在这两种风格中,采用 QUERY 的理由是一样的:让搜索类读取请求重新获得 HTTP 层面的缓存和重试能力。
QUERY 实现端到端可用还需要什么
要让 QUERY 在所有环境中可用,用户与处理函数之间的每一层都必须能识别它:
- HTML 规范需要接受
method="query",浏览器也需要实现它。 - 每个节点(源服务器、反向代理、负载均衡器、CDN、API 网关和 WAF)都需要能解析并转发该方法。
- 框架需要能把 QUERY 路由到处理函数,并解析其请求体。
- 缓存在存储 QUERY 响应之前,需要把请求体和 Content-Type 纳入缓存键。
CORS 预检是有意为之的设计:RFC 10008 预期会有预检,whatwg/fetch#1938 也没有要求修改安全列表。该 issue 中尚未解决的问题是,Fetch 以 URL 为键的 HTTP 缓存是否应该为 QUERY 支持以请求体为键的缓存。
结语
QUERY 解决了 HTTP 中一个长期存在的矛盾:搜索请求可以携带请求体,同时保留缓存和安全重试的能力。脚本、Go 客户端和 Laravel 应用现在就可以使用它。表单还不行,而且不认识该方法的基础设施仍可能将其拒绝。在迁移接口之前,请先让一个真实的 QUERY 请求走完整条生产链路,包括 CDN、网关和 WAF。在链路上的每个节点都能放行它之前,请保留原有的 POST 接口。
常见问题
为什么不直接在 GET 请求中携带请求体,而要使用 QUERY?
GET 请求体并不可靠。RFC 9110 第 9.3.1 节指出,GET 请求中的请求体没有通用的明确语义,也不能改变请求的目标或含义。一些服务器会直接拒绝这类请求,因为 GET 请求体可能被用于请求走私(request smuggling)攻击。此外,请求体也不属于标准缓存键的一部分。QUERY 则把请求体本身定义为查询,并将其纳入缓存键。
QUERY 方法与 WebDAV 的 SEARCH 方法有什么区别?
SEARCH 是 2008 年在 RFC 5323 中定义的 WebDAV 方法,用于通过 XML 查询语法搜索 DAV 资源。QUERY 则是适用于任意查询格式的通用 HTTP 方法。两者都是安全方法,但 SEARCH 从未在 WebDAV 之外普及。QUERY 规范的早期草案曾使用 SEARCH 这个名称。作者后来改用 QUERY,一方面是因为 SEARCH 和其他现有的安全方法都源自 WebDAV,并依赖通用的 XML 媒体类型;另一方面是因为 QUERY 这个名称与 URI 中的查询(query)部分相对应。
可以在 OpenAPI 中描述 QUERY 接口吗?
可以,从 2025 年 9 月发布的 OpenAPI 3.2.0 开始支持。该版本内置了对 query 方法的支持,因此 QUERY 操作可以像 get 或 post 一样声明 requestBody schema 和 responses。其他非标准方法则放在新增的 additionalOperations 映射中。OpenAPI 3.0 和 3.1 没有对应 QUERY 的字段,而各类代码生成器对 3.2 的支持程度也不尽相同。
服务器不支持 QUERY 时会返回什么状态码?
根据 RFC 9110,服务器无法识别请求方法时应返回 501 Not Implemented。如果服务器能识别 QUERY,但不允许在某个资源上使用它,则返回 405 Method Not Allowed,并通过 Allow 头列出所支持的方法。代理和 WAF 的失败方式可能不同,例如直接返回 400。因此,任何回退到 POST 的逻辑都应同时处理这三种情况。