是的,go 的 http.client 默认自动跟随重定向(最多10次),但会丢失中间响应头、无法干预跳转逻辑;禁用需设 checkredirect 返回 http.erruselastresponse。

Go 的 http.Client 默认会自动跟随重定向吗?
是的,http.DefaultClient 和新创建的 http.Client{} 默认都会自动处理 301/302/307/308 等重定向响应,最多跟随 10 次(由 Client.CheckRedirect 控制)。这不是“魔法”,而是底层调用了默认的 http.DefaultRedirectPolicy。
但这也正是问题源头:自动跟随意味着你无法在重定向链中 inspect 响应头、修改请求头、记录跳转路径,甚至可能因循环重定向导致 panic。
- 默认行为下,
resp.StatusCode返回的是最终响应码(如 200),而非原始 302 - 中间跳转的
Location头、Set-Cookie、Authorization等全部丢失 - 若服务端返回 307 或 308,且重定向目标是 POST 请求,Go 默认会**保留原始方法和 body**;但 301/302 会被强制转为 GET(丢弃 body)——这点常被忽略
如何禁用自动重定向并手动控制跳转逻辑?
通过设置 Client.CheckRedirect 字段为自定义函数,并返回 http.ErrUseLastResponse,即可中断自动跳转,拿到原始重定向响应。
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // 阻止继续跳转
},
}
resp, err := client.Do(http.NewRequest("GET", "https://example.com/redirect", nil))
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
fmt.Printf("Redirect to: %s (status: %d)\n", location, resp.StatusCode)
// 此时可手动构造新请求、添加 header、复用 cookie 等
}
注意:http.ErrUseLastResponse 是唯一能终止跳转并保留当前 *http.Response 的方式;返回 nil 会继续跳,返回其他 error(如 fmt.Errorf("stop"))会导致 Do() 直接失败。
立即学习“go语言免费学习笔记(深入)”;
如何安全地实现带状态的重定向链(如携带 Cookie、Header)?
手动跳转时,必须显式传递上下文状态。常见遗漏点是:Cookie 不自动携带、Authorization 头丢失、Host 被重写、body 未重放(对 POST/PUT)。
- 使用
http.CookieJar可让 Client 自动管理 Cookie(包括跨跳转):传入实现了http.CookieJar接口的实例(如cookiejar.New(nil)) - 手动构造后续请求时,需复制原始请求的
Header(除Host、Content-Length等由 net/http 自动设置的字段外) - 对非 GET/HEAD 方法,检查
resp.StatusCode类型:307/308 应原样重放 body;301/302/303 必须转为 GET(除非你明确要违反 RFC) - 重定向 URL 可能是相对路径,需用
resp.Request.URL.ResolveReference()解析
例如解析跳转地址:
u, err := resp.Request.URL.Parse(resp.Header.Get("Location"))
if err != nil {
return err
}
nextReq, _ := http.NewRequestWithContext(resp.Request.Context(), "GET", u.String(), nil)
// 复制必要 header(如 Authorization)
nextReq.Header.Set("Authorization", resp.Request.Header.Get("Authorization"))
重定向时 Body 读取不完整导致后续请求失败?
这是最隐蔽的坑:当你调用 resp.Body.Read() 或 ioutil.ReadAll()(Go 1.16+ 用 io.ReadAll())后,resp.Body 已关闭或耗尽。若之后想重放 body(比如 POST 重定向到 307),就会得到空内容。
解决方案只有两个:
- 在首次读取前,用
httputil.DumpResponse(resp, false)或io.Copy(ioutil.Discard, resp.Body)吃掉 body(仅用于调试或丢弃) - 若需重放,必须在
Do()前将 body 缓存为bytes.Reader或strings.NewReader(),并在每次重试时重新赋值给req.Body
例如:
bodyBytes, _ := io.ReadAll(originalReq.Body) originalReq.Body = io.NopCloser(bytes.NewReader(bodyBytes)) // 后续重放时: nextReq.Body = io.NopCloser(bytes.NewReader(bodyBytes))
别忘了 req.Body 必须实现 io.ReadCloser,所以要用 io.NopCloser 包一层。
重定向不是简单地换 URL 再发一次请求,它牵扯状态延续、方法语义、header 传播和 body 生命周期。多数问题出在假设“自动跳转很安全”或“手动跳转只要改 URL 就行”。实际项目里,建议优先用 CheckRedirect + CookieJar 组合,再按需注入逻辑,而不是从零手写跳转循环。










