12k
All articles

HTTP 迎来一个专为搜索设计的新方法

了解 HTTP QUERY:一种通过请求正文执行搜索的安全、幂等方法。查看缓存与重试机制、支持它的客户端,以及可能拦截请求的基础设施。

OpenReplay Team
OpenReplay Team
HTTP 迎来一个专为搜索设计的新方法

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 在所有环境中可用,用户与处理函数之间的每一层都必须能识别它:

  1. HTML 规范需要接受 method="query",浏览器也需要实现它。
  2. 每个节点(源服务器、反向代理、负载均衡器、CDN、API 网关和 WAF)都需要能解析并转发该方法。
  3. 框架需要能把 QUERY 路由到处理函数,并解析其请求体。
  4. 缓存在存储 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 的逻辑都应同时处理这三种情况。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.