HTTP QUERY方法详解:RFC 10008、生态采用与Quarkus实现
当你构建搜索 API 时,通常会从 HTTP GET 开始,因为它是读操作的自然选择:安全、幂等且可缓存。接着搜索表单不断增长,过滤器不断增加,嵌套条件也随之出现。由于使用 GET 意味着将查询放入 URI 中,长度限制问题便出现了。更糟糕的是,将敏感的查询值放入 URI 会增加通过访问日志、浏览器历史、代理和监控系统暴露的机会。由于 HTTP 协议并不禁止,GET 携带请求体看起来像是一条出路,但把设计建立在标准未定义的行为之上并不是值得推荐的做法。Elasticsearch 的 GET 携带请求体搜索 API 就是一个众所周知的例子,Elastic 自己的文档也公开承认了这个问题:
“因此,有些 HTTP 服务器允许这样做,有些——尤其是缓存代理——则不允许。[……] 然而,由于带请求体的 GET 并未被普遍支持,搜索 API 也接受 POST 请求。”
而 HTTP POST 则将查询放在请求负载中,而非 URI 中,这既解决了长度限制问题,也解决了数据泄露问题。但 POST 既不是安全的,也不是幂等的,因为协议允许每次调用都改变服务器上的状态,而且除非带有明确的新鲜度信息,否则其响应不会被缓存。POST 的这种特性还带来了性能开销:每次调用都会重新计算结果并重新传输,而且超时的请求无法安全地重试。
缺少什么很清楚:需要一个像 GET 一样安全且幂等,但又像 POST 一样携带内容的方法。直到 2026 年 6 月,HTTP 才以标准化形式提供了这样的方法。
QUERY 方法
为了满足这一需求,IETF 在 RFC 10008 中引入了 QUERY 方法。这是自 2010 年 RFC 5789 标准化以来第一个新的 HTTP 方法。
其核心思想可以概括为:QUERY 请求要求目标资源以安全且幂等的方式处理所包含的内容,并以结果作为响应。RFC 引入的其他内容要么源自这一定义,要么围绕它构建实用机制。让我们逐一看看关键概念:
安全与幂等
QUERY 被定义为一种安全操作:它不请求改变目标资源的状态。它可以被自动重试、重复或重新开始,而无需担心部分副作用。这正是将它和 POST 区分开来的契约。
含义来自 Content-Type
RFC 10008 刻意不定义查询语言。同一个端点可以接受 JSON 过滤文档、表单编码字符串或任何由媒体类型定义的查询语言;请求内容的媒体类型定义了服务器应如何解释它。服务器必须拒绝 Content-Type 缺失或与内容不一致的请求。RFC 甚至禁止内容嗅探:服务器不允许从请求内容推断媒体类型,并用它来修复缺失或错误的 Content-Type。
明确可缓存
与 POST 不同,QUERY 为携带请求体的请求引入了可缓存性,但有一个至关重要的变化:缓存键必须同时包含 URI 和请求内容,因为对同一 URI 发送不同请求体的两个 QUERY 请求是不同的查询。
通过 Accept-Query 进行发现
服务器可以通过 Accept-Query 响应头来宣告其对 QUERY 的支持,该响应头列出了它接受作为查询内容的媒体类型。
等价资源
QUERY 响应可能包含一个 Location 头,指向表示相同查询的 URI。客户端之后可以使用普通 GET 重新获取结果,无需请求体。规范还为 303 See Other 赋予了自然的角色,用于将查询重定向到可检索的资源。RFC 的安全注意事项还增加了一个告诫:当查询包含不得被记录到日志中的敏感信息时,分配给此类资源的 URI 不应包含原始查询内容中的任何敏感部分,否则 QUERY 所避免的暴露问题会在一次响应之后重新出现。
熟悉的错误语义
RFC 针对失败情况推荐了具体的状态码:媒体类型信息缺失时返回 400,资源不支持该媒体类型时返回 415,内容格式正确但无法处理该查询时返回 422。
十年磨一剑
RFC 经历了漫长的历程。这一想法可以追溯到 WebDAV 的 SEARCH 方法(RFC 5323,2008 年),它证明了对请求体驱动的查询的需求,但仅限于基于 XML 的 WebDAV 生态系统。2021 年,HTTP 工作组将该工作采纳为工作组项目,使其从个人提案进入 IETF 标准化流程。后来,该方法从 SEARCH 更名为 QUERY,以避免与现有的 WebDAV SEARCH 方法混淆,并更好地反映其用途。该文档于 2026 年 6 月作为 RFC 10008 发布。从初稿到成为拟议标准(Proposed Standard)历时十一年,这很好地提醒我们,即便是对 HTTP 看似简单的添加,也会触及庞大的已安装基础,因此需要经过广泛的审查。
当前生态系统的支持状况
截至 2026 年 7 月,HTTP QUERY 已通过 RFC 10008 完成标准化阶段,但生态系统的采用仍处于早期阶段。许多 HTTP 服务器和代理可以在不更改协议的情况下转发 QUERY 请求,但跨框架、浏览器 API、缓存、WAF 和 API 工具的原生支持仍在逐渐出现。主要障碍不再来自协议本身,而是庞大的、假设 HTTP 方法集固定的软件安装基础。
Java 生态系统提供了一个关于采用进展的有用快照:
Apache Tomcat
添加 QUERY 支持的拉取请求已于 2026 年 7 月 1 日合并(apache/tomcat#1026)。该支持仅在 Tomcat 12 中可用,因为它需要修改 Servlet API。
Eclipse Jetty
Eclipse Jetty 有一个开放的拉取请求(jetty/jetty.project#15316),实现了 RFC 10008 的核心语义:将方法注册为安全且幂等、Accept-Query 头、重定向行为,以及与压缩和缓冲处理器的集成。最初计划面向 Jetty 12.1,但已重新调整为面向 Jetty 13,与可能的 Jakarta Servlet 6.2 时间线保持一致。
Jakarta Servlet
有一个开放的问题(jakartaee/servlet#1068)提议将 QUERY 添加到规范本身,以便 HttpServlet 获得一流的支持,并且 QUERY 请求获得当前为 POST 定义的表单参数处理模型。这可以说是整个 Jakarta EE 生态系统中最重要的里程碑,因为它将 QUERY 从特定于容器的支持提升到平台规范本身。一旦 Servlet 定义了 QUERY,WildFly、Payara 和 Open Liberty 等应用服务器在升级到新的规范级别时,就可以通过其 Servlet 容器继承该支持。截至本文撰写时,它们都没有在规范之前发布 QUERY 支持。
Spring 呢?
Spring 值得单独一节,因为它的请求映射建模方式不同。Spring MVC 和 WebFlux 通过 RequestMethod 枚举公开其基于注解的请求映射模型,该枚举目前包含 GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS 和 TRACE。没有 RequestMethod.QUERY,这意味着目前你无法通过 Spring 基于注解的编程模型以声明方式映射 QUERY 请求。现有的变通方案很笨拙,并且绕过了 Spring 正常的请求映射模型:声明一个通用映射,手动检查 request.getMethod(),或者实现自定义的 RequestMappingHandlerMapping。
与 Servlet 的情况不同,这主要不是容器问题,而是框架 API 和抽象问题。Spring 团队已经意识到了这一点。一个添加 QUERY 支持的社区拉取请求(spring-projects/spring-framework#34993)自 RFC 10008 发布之前就已经开启。它取代了一个已开放近两年的功能请求,并且维护者表示有意将目标定为 Spring Framework 7.1(目前预计于 2026 年 11 月发布)。甚至还有一个命名冲突需要先解决:显而易见的便捷注解 @QueryMapping 已被 Spring 用于 GraphQL。
为什么 Quarkus 今天就能做到
这里体现了 HTTP 一个未被充分重视的特性所带来的回报:请求方法只是由 HTTP 语法定义的一个标记。服务器不需要内置对每个方法的了解就能解析它。Quarkus 将其 HTTP 层构建在 Netty 和 Vert.x 之上,两者都不要求方法必须是预定义集合中的一员;请求可以在不需要对 QUERY 进行特殊处理的情况下到达路由层。除此之外,Jakarta REST 自 JAX-RS 1.0 起就为自定义方法提供了标准的扩展点:@HttpMethod 元注解,这一机制多年来一直让 JAX-RS 应用能够暴露诸如 PROPFIND 之类的 WebDAV 方法。
将两者结合起来,Quarkus 中兼容 RFC 10008 的 QUERY 端点不需要修改框架;只需通过一个 Jakarta REST 扩展点即可启用:
剩下的工作是在应用层实现 RFC 10008 语义,这正是示例项目所展示的。
示例:一个可以用 QUERY 查询的产品目录
演示仓库可在 GitHub 上获取:hakdogan/http-query-method。这是一个小型的 Quarkus 应用,在 /products 处暴露了一个产品目录。它刻意保持紧凑,只有几个类,但 RFC 10008 的每一个概念都在代码中有具体的对应实现。
一个查询,两种媒体类型
该资源接受同一逻辑过滤器的两种表示形式,展示了查询语义是由 Content-Type 决定的,而不是 URI:
因此,以下两种方式都可以正常工作,并且含义相同:
带有不受支持媒体类型的请求会被拒绝并返回 415;格式正确但自相矛盾的过滤器(例如 minPrice 大于 maxPrice)会返回 422。后一种情况是设计选择而非 RFC 要求:第 2.1 节指出,当内容与其媒体类型匹配但因实际内容而无法处理查询时,可以使用 422;而返回 200 和空结果同样是合理的解释。演示将这种矛盾视为客户端错误,因为空的 200 响应将无法与合法的空匹配结果区分开来,从而静默地隐藏几乎可以肯定是调用者中的 bug。
响应说明了一切
一个成功的 QUERY 返回如下:
三个头承载了 RFC 的核心思想:
- Accept-Query 宣告资源接受哪些媒体类型作为查询内容。在演示中,它由一个小的响应过滤器添加。
- Location 指向 RFC 第 2.2 节中的等价资源:通过请求 URI 表达的相同查询。使用普通 GET 获取它,你会得到完全相同的结果,无需请求体。其中一个测试正是这样做的往返验证。
- Cache-Control 和 ETag 使可缓存性的承诺具体化。ETag 来源于结果,因此使用
If-None-Match重复查询会返回304 Not Modified,而无需重新发送结果:
这就是对“为什么不直接用 POST”的回答:QUERY 的设计目的是在提供查询语义的同时,不放弃与安全方法相关的缓存友好特性。
无需预先了解的发现机制
客户端如何发现资源支持 QUERY?一个 OPTIONS 请求:
响应通过两个头来回答,一个列出资源接受的方法,另一个列出资源接受作为查询内容的媒体类型:
在这种情况下,Quarkus 自动生成了 Allow 头,其中包含 QUERY,仅仅因为一个资源方法绑定到了它。
证明幂等性
演示的测试套件涵盖了过滤逻辑、媒体类型处理、错误码、等价资源往返、条件请求流程,并且(对于一个定义特征为可重复性的方法来说相当贴切)还有一个测试会重复执行同一查询多次,以验证操作保持安全并产生一致的响应。
这个示例的关键经验不是 QUERY 是如何实现的,而是为什么可以实现:HTTP 扩展点早已存在,框架无需发明新的抽象。
结论
QUERY 并不是一场革命;它只是许多系统多年来通过基于 POST 的查询端点所实现的一种模式的标准化。这正是它重要的原因。在“能用”和“在协议所给予的保证下能用”之间的差距,正是缓存、幂等重试和更好工具得以实现的空间。
采用是不均衡的:首先出现在协议实现和服务器中,然后出现在框架、网关和 CDN 中。但正如示例所示,在像 Quarkus 这样的技术栈上,方法被视为可扩展的值,而不是硬编码的列表,你无需等待即可开始尝试。
协议已经准备好进行扩展;有趣的问题是,其上的各层是否保留了这种灵活性。
完整示例(包括所有测试)可在 GitHub 上获取:hakdogan/http-query-method。
参考
- RFC 10008, The HTTP QUERY Method: https://www.rfc-editor.org/info/rfc10008/
- IETF Datatracker, document history: https://datatracker.ietf.org/doc/rfc10008/
- RFC 9110, HTTP Semantics: https://www.rfc-editor.org/info/rfc9110/
- RFC 4918, WebDAV: https://www.rfc-editor.org/info/rfc4918/
- RFC 5323, WebDAV SEARCH: https://www.rfc-editor.org/info/rfc5323/
- RFC 5789, PATCH: https://www.rfc-editor.org/info/rfc5789/
本文《HTTP QUERY Method Explained: RFC 10008, Ecosystem Adoption, and a Quarkus Implementation》最初发布在 foojay。