Go net/http 新增 MethodQuery 常量:RFC 10008 QUERY 方法的支持与重定向行为解析
本文围绕 Go 标准库 net/http 包新增的 MethodQuery 常量展开:介绍 RFC 10008 定义的 HTTP QUERY 方法语义,说明 MethodQuery 在源码中的定义位置与 API 变更记录,并结合 client.go 中 redirectBehavior 函数与 client_test.go 的测试用例,讲清 Go 客户端在遇到 QUERY 请求的重定向(301/302/303/307/308)时如何处理方法与请求体。读完后,你可以在 Go 代码中直接使用 http.MethodQuery 构造、路由 QUERY 请求,并准确预判 http.Client 的重定向行为。
一、变更背景:QUERY 方法进入 Go 标准库
官方变更说明记录在 doc/next/6-stdlib/99-minor/net/http/80058.md 中,核心内容只有一句话:
新增的
MethodQuery常量表示 RFC 10008 中定义的 HTTP QUERY 方法。
对应的 API 变更条目保存在 api/next/80058.txt:
pkg net/http, const MethodQuery = "QUERY" #80058
pkg net/http, const MethodQuery ideal-string #80058
RFC 10008 为 HTTP 增加了一组面向"结构化查询"的方法,其中 QUERY 的核心定位是:客户端向服务器提交一个查询描述符(query descriptor)作为请求体,服务器按该描述符检索数据并返回。与 POST 不同,QUERY 在语义上被定义为安全的(safe)且幂等的(idempotent) 请求方法,这一点直接决定了 Go http.Client 后续的重定向策略(见第四节)。
二、MethodQuery 常量在 net/http 中的定义
所有 HTTP 方法常量集中定义在 src/net/http/method.go,新增的 MethodQuery 被追加在这一常量块的末尾:
// Common HTTP methods.
//
// Unless otherwise noted, these are defined in RFC 7231 section 4.3.
const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH" // RFC 5789
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
MethodQuery = "QUERY" // RFC 10008
)
有两个细节值得注意:
- 注释区分了出处。
MethodPatch标注 RFC 5789,MethodQuery标注 RFC 10008,其余方法默认出自 RFC 7231 第 4.3 节——MethodQuery不属于经典七个方法之列,属于标准库对新规范方法的增量支持。 - 常量类型是理想的字符串(ideal string)。从
api/next/80058.txt的ideal-string标记可以看到,MethodQuery是未指定类型的字符串常量,因此既可以赋给string变量,也可以直接传给http.NewRequest等需要string的 API,并且参与编译期常量检查(例如误拼方法名会在 switch 比较场景中暴露)。
在业务代码中的典型用法:
// 构造一个 QUERY 请求
req, err := http.NewRequest(http.MethodQuery, "https://example.com/search", strings.NewReader(queryDescriptor))
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(req)
在服务器端,r.Method 会原样取到字符串 "QUERY",因此路由匹配时可以直接使用 http.MethodQuery 做比较,避免散落裸字符串:
http.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodQuery:
// 解析 r.Body 中的查询描述符,执行检索
case http.MethodGet:
// 简单查询走 query string
}
})
此外,从源码结构看,client.go 中的 urlErrorOp 函数会把方法名转成错误信息中的操作名(大写转首字母小写),因此 QUERY 请求失败时,*url.Error 的 Op 字段会呈现为 "Query",这为日志排查提供了与方法一一对应的线索。
三、QUERY 的方法语义:安全且幂等
理解这次变更的关键,是 QUERY 与 POST 的本质差异:
| 特性 | POST(参照系) | QUERY(RFC 10008) |
|---|---|---|
| 是否携带请求体 | 是 | 是(查询描述符) |
| 是否 safe | 否 | 是(不改变服务器状态) |
| 是否 idempotent | 否 | 是(重复查询结果一致) |
正因为"safe + idempotent",RFC 10008 第 2.5 节允许对 QUERY 的重定向沿用保留方法与请求体的处理方式。Go 客户端正是依据这一节实现了下面的逻辑。
四、客户端重定向行为:redirectBehavior 源码解析
Go http.Client 处理 3xx 重定向的核心函数是 client.go 中的 redirectBehavior(位于 checkRedirect 之后,约 L498 起):
// redirectBehavior describes what should happen when the
// client encounters a 3xx status code from the server.
func redirectBehavior(reqMethod string, resp *Response, ireq *Request) (redirectMethod string, shouldRedirect, includeBody bool) {
switch resp.StatusCode {
case 301, 302, 303:
redirectMethod = reqMethod
shouldRedirect = true
includeBody = false
// RFC 10008, Section 2.5: QUERY is safe and idempotent, so 301
// and 302 preserve the method and re-send the body (as 307 and
// 308 do); only 303 changes the method to GET.
if reqMethod == "QUERY" && resp.StatusCode != 303 {
includeBody = true
if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
// We had a request body, and 301/302 require
// re-sending it, but GetBody is not defined.
shouldRedirect = false
}
} else if reqMethod != "GET" && reqMethod != "HEAD" {
// RFC 2616 allowed automatic redirection only with GET and
// HEAD requests. RFC 7231 lifts this restriction, but we still
// restrict other methods to GET to maintain compatibility.
// See Issue 18570.
redirectMethod = "GET"
}
case 307, 308:
redirectMethod = reqMethod
shouldRedirect = true
includeBody = true
// ...
}
return redirectMethod, shouldRedirect, includeBody
}
逐条拆解这段分支逻辑:
- 默认路径(301/302/303):方法保留,请求体丢弃。若原方法既不是 GET 也不是 HEAD,则把方法降级为 GET——这是为兼容 RFC 2616 时代行为的保守策略(见 Issue 18570 注释)。
- QUERY 特殊分支:当
reqMethod == "QUERY"且状态码不是 303 时,includeBody = true,即 301 和 302 会保留 QUERY 方法并原样重发请求体,行为与 307/308 对齐。注释明确引用了 RFC 10008 第 2.5 节作为依据。 - 303 的例外:303 See Other 会把方法改为 GET(走上面的
reqMethod != "GET"分支),因为 303 的语义就是"用 GET 去 Location 取结果"。 - GetBody 缺失时的兜底:如果请求携带了请求体(
outgoingLength() != 0)但没有设置GetBody(即无法重放请求体),则shouldRedirect = false,客户端放弃自动跳转,把当前 3xx 响应直接返回给调用方,而不是报错。这一兜底与 307/308 分支的处理完全一致,保证 QUERY 在"请求体不可重放"场景下的行为可预期。
汇总成行为对照表:
| 原方法 | 301 | 302 | 303 | 307 | 308 |
|---|---|---|---|---|---|
| GET / HEAD | 保留方法,无体 | 保留方法,无体 | 变 GET,无体 | 保留方法,带体 | 保留方法,带体 |
| 其他方法(含旧行为) | 变 GET,无体 | 变 GET,无体 | 变 GET,无体 | 保留方法,带体 | 保留方法,带体 |
| QUERY(本次变更) | 保留 QUERY,带体 | 保留 QUERY,带体 | 变 GET,无体 | 保留 QUERY,带体 | 保留 QUERY,带体 |
对使用者而言,这意味着:只要你的 QUERY 请求通过 http.NewRequest 构造(该路径会默认填充 GetBody),遇到 301/302 时客户端会自动带着查询描述符跳转到新地址重新查询,无需手工处理;而如果你用 Request 字面量手工构造且未设置 GetBody,遇到需要重放请求体的重定向时,Client 会直接把 3xx 响应交还给你,由你决定是否手动跟随。
五、测试用例验证:TestQueryRedirects
上述行为在 client_test.go 的 TestQueryRedirects(约 L404)中有完整覆盖。测试通过 testRedirectsByMethod(t, mode, "QUERY", ...) 复用既有的重定向测试框架,期望的请求序列(节选)清晰地演示了跳转链上方法与请求体的演变:
QUERY / "first" // 初始请求,带体 "first"
QUERY /?code=302&next=302 "c302" // 302 -> 保留 QUERY 与体
QUERY /?code=302 "c302" // 继续 302 跳转
QUERY / "c302"
QUERY /?code=301&next=302,308 "c301" // 301 -> 保留 QUERY 与体
QUERY /?code=302&next=308 "c301"
QUERY /?code=308 "c301"
QUERY / "c301"
QUERY /?code=303&next=301 "c303" // 303 -> 方法改为 GET
GET /?code=301 ""
GET / ""
QUERY /?code=307&next=303,302 "c307" // 307 -> 保留 QUERY 与体
QUERY /?code=303&next=302 "c307"
GET /?code=302 ""
GET / ""
QUERY /?code=404 "c404" // 404 终止跳转链
从断言序列可以确认三件事:301/302/307/308 全程保持 QUERY 方法和请求体;303 把方法切换为 GET 且体被丢弃;遇到非 3xx(如 404)时跳转链终止。测试以 http3SkippedMode 运行,即 HTTP/3 模式下该组用例会跳过,这是该测试框架对所有重定向测试的统一约定,与本次变更本身无关。
六、使用要点与适用前提
- 适用版本:
MethodQuery属于当前开发线api/next中的新增常量(变更文档位于 doc/next 的 minor 变更目录),使用它需要构建包含该变更的工具链;在更早版本中若确有 QUERY 场景,只能使用字符串字面量"QUERY"。 - 客户端:使用
http.Client.Do时,QUERY 的 301/302 保留语义依赖请求体可重放(GetBody非空);http.NewRequest构造的请求会自动设置GetBody,是推荐做法。 - 服务器端:
net/http的服务器不会校验方法白名单,任何方法(包括 QUERY)都能被ServeMux/自定义 Handler 接收,行为取决于你自己的路由代码;标准库层面本次变更只是补齐了方法常量与客户端重定向语义。 - 互操作提示:QUERY 并非所有中间件、代理、HTTP 客户端都认识的方法,跨系统集成前建议确认对端(如反向代理、WAF)是否会放行非标准七法之外的方法。
七、小结
本次变更(doc/next/6-stdlib/99-minor/net/http/80058.md)以极小的 API 面——一个常量——把 RFC 10008 的 QUERY 方法纳入了 Go 标准库的方法体系,其真正的技术含量落在 client.go 的 redirectBehavior:QUERY 因"安全且幂等"而获得与 307/308 相同的重定向待遇(301/302 保留方法与请求体),仅 303 例外转为 GET。配合 client_test.go 中 TestQueryRedirects 的端到端断言,这一语义既有实现、也有回归保障,可以放心在生产代码中作为常量引用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00