HTTP QUERY方法详解:RFC 10008、生态采用与Quarkus实现
然后,搜索表单逐渐变得复杂,过滤器不断增多,嵌套条件也随之出现。由于使用 GET 意味着将查询放在 URI 中,因此会出现长度限制问题。更糟糕的是,将敏感查询值放在 URI 中,会通过访问日志、浏览器历史、代理和监控系统增加泄露风险。由于 HTTP 协议并未禁止,携带请求体的 GET 请求看起来像是一种出路,但把设计建立在标准未定义的行为之上并不是推荐做法。Elasticsearch 携带请求体的 GET 搜索 API 就是一个众所周知的例子,Elastic 官方文档也公开承认了这个问题:
因此,一些 HTTP 服务器允许这样做,而另一些——尤其是缓存代理——则不允许。[...] 然而,由于带请求体的 GET 并未被普遍支持,搜索 API 也接受 POST 请求。
另一方面,HTTP POST 将查询放在请求负载中而不是 URI 中,这同时解决了长度限制和数据泄露问题。但 POST 既不是安全的,也不是幂等的,因为协议允许每次调用都改变服务器上的状态,并且除非带有明确的新鲜度信息,否则其响应不会被缓存。POST 的这一特性也带来了性能开销:每次调用都会重新计算结果并重新传输,而且超时的请求无法安全地重试。
缺失的东西很明确:一个像 GET 一样安全且幂等,但又像 POST 一样携带内容的方法。在 2026 年 6 月之前,HTTP 一直没有以标准化形式提供这样的方法。为了满足这一需求,IETF 在 RFC 10008 中引入了 QUERY 方法。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 发布。从首个草案到成为拟议标准,历经十一年,这很好地提醒我们:即使是对 HTTP 看似简单的补充,也会触及庞大的既有安装基础,因此会得到广泛而严格的审查。
生态系统支持现状
截至 2026 年 7 月,HTTP QUERY 已通过 RFC 10008 完成标准化阶段,但生态系统的采用仍处于早期阶段。许多 HTTP 服务器和代理可以在不更改协议的情况下转发 QUERY 请求,但跨框架、浏览器 API、缓存、WAF 和 API 工具的原生支持仍在逐步涌现。主要障碍不再是协议本身,而是大量假定 HTTP 方法为固定集合的既有软件。Java 生态系统提供了一个关于采用进程的有用快照:
Apache Tomcat
一个为 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
有一个开放式 issue(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 响应与合法的空匹配结果无法区分,会悄悄隐藏掉调用方中几乎可以肯定是错误的问题。
响应说明了一切
一次成功的 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 并验证操作保持安全且响应一致的测试。
这个示例的关键经验不在于 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/