重定向
HTTPC 默认自动跟随 HTTP 重定向(最多 10 次),并记录完整的重定向链。本页覆盖跟随控制、次数限制、链路追踪、状态码方法语义、跨域凭据剥离、环形重定向检测、域名白名单以及与 SSRF 防护的联动。
默认行为
不进行任何配置时,客户端自动跟随重定向,直到到达最终响应或达到次数上限:
package main
import (
"fmt"
"log"
"github.com/cybergodev/httpc"
)
func main() {
result, err := httpc.Get("https://httpbin.org/redirect/2")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.StatusCode()) // 200(最终响应的状态码)
fmt.Println(result.Meta.RedirectCount) // 2(实际跟随的重定向次数)
}引擎自动跟随 301/302/303/307/308 五种重定向状态码,方法语义遵循 HTTP 规范:
| 状态码 | 方法处理 |
|---|---|
| 301 / 302 / 303 | 可能将 POST 改写为 GET(规范允许) |
| 307 / 308 | 保持原方法与请求体重发 |
状态码语义(301/302/303/307/308)
五种状态码在「方法改写」与「请求体」上的差异如下:
| 状态码 | 含义 | 方法处理 | 请求体 | 典型用途 |
|---|---|---|---|---|
| 301 | 永久移动 | GET/HEAD 不变;POST 等改写为 GET | 丢弃 | 域名迁移、URL 规范化 |
| 302 | 临时移动(Found) | 同 301(沿用事实标准行为) | 丢弃 | 临时跳转、登录后转向 |
| 303 | 参见其他(See Other) | 一律改为 GET | 丢弃 | POST 后跳转结果页 |
| 307 | 临时重定向 | 保持原方法 | 保留重发 | 需重发 body 的临时跳转 |
| 308 | 永久重定向 | 保持原方法 | 保留重发 | 需重发 body 的永久跳转 |
带请求体的 307/308 不会自动跟随
307/308 要求按原方法重放请求体。底层 net/http 只有在请求体可重放(设置了 GetBody)时才会跟随,而 HTTPC 构建请求时未设置该函数——因此携带非空请求体的请求收到 307/308 时不自动跟随,3xx 响应原样返回(不报错)。需要跟随这类跳转时,用 WithFollowRedirects(false) 手动循环处理,或让服务端改用 302/303。
300/304 与其他 3xx 不在跟随范围
即使携带 Location 头,引擎也只跟随 301/302/303/307/308;300(Multiple Choices)、304(Not Modified)及其他 3xx 响应原样返回,由调用方自行处理。注意 Result.IsRedirect() 判断的是整个 300–399 区间,与是否自动跟随无关。
超出上限即报错
重定向次数超过 MaxRedirects 时请求以错误结束(错误信息形如 stopped after 3 redirects),不会无限循环。MaxRedirects 合法范围 0-50,越界会在配置校验时报错。
控制跟随
重定向配置分布在三个层级,作用范围从大到小:
| 层级 | 配置 | 作用范围 |
|---|---|---|
| 客户端级 | Config.Defaults.FollowRedirects / MaxRedirects | 整个客户端的所有请求 |
| 请求级 | WithFollowRedirects(bool) / WithMaxRedirects(n) | 单次请求,覆盖客户端配置 |
| 预设 | SecureConfig()、MinimalConfig()(均置 FollowRedirects=false) | 安全/极简场景默认不跟随 |
客户端级
Config.Defaults(RequestDefaults)为整个客户端设置重定向策略:
cfg := httpc.DefaultConfig()
cfg.Defaults.FollowRedirects = true // 默认:跟随
cfg.Defaults.MaxRedirects = 5 // 默认:10
client, err := httpc.New(cfg)请求级
WithFollowRedirects / WithMaxRedirects 覆盖单次请求的客户端配置:
// 仅这一次请求禁止跟随,直接拿到 3xx 响应
result, err := httpc.Get("https://httpbin.org/redirect/1",
httpc.WithFollowRedirects(false),
)
if result.IsRedirect() {
fmt.Println(result.Response.Headers.Get("Location")) // 重定向目标地址
}
// 仅这一次请求限制跟随 3 次
result, err = httpc.Get(url, httpc.WithMaxRedirects(3))MaxRedirects(0) 不是禁用
0 是「未设置」哨兵值——引擎回退到默认值 10,而不是禁止重定向。要禁止跟随,用 WithFollowRedirects(false) 或 Config.Defaults.FollowRedirects = false。
SecureConfig 默认禁止重定向
SecureConfig() 预设将 FollowRedirects 置为 false,防止借助重定向把请求导向内部地址(重定向型 SSRF)。安全细节见 SSRF 防护。
追踪重定向链
Result.Meta 记录每次请求的重定向信息:
| 字段 | 说明 |
|---|---|
Meta.RedirectCount | 实际跟随的重定向次数 |
Meta.RedirectChain | 重定向过程中经过的 URL 序列 |
两个字段的精确语义:
RedirectChain记录的是每一跳的来源 URL:第一项是初始请求 URL,之后依次是各中间 URL;最终目标 URL 不在链中。需要最终地址时读result.Request.URL(跟随完成后的最终请求 URL)。RedirectCount恒等于len(RedirectChain)。
package main
import (
"fmt"
"log"
"github.com/cybergodev/httpc"
)
func main() {
result, err := httpc.Get("https://httpbin.org/redirect/3")
if err != nil {
log.Fatal(err)
}
fmt.Printf("跟随 %d 次重定向\n", result.Meta.RedirectCount)
for i, u := range result.Meta.RedirectChain {
fmt.Printf(" %d. %s\n", i+1, u)
}
fmt.Println("最终地址:", result.Request.URL)
// 输出:
// 1. https://httpbin.org/redirect/3
// 2. https://httpbin.org/redirect/2
// 3. https://httpbin.org/redirect/1
// 最终地址: https://httpbin.org/get
}跨域凭据自动剥离
跟随重定向时,引擎在每一跳检查目标主机名:与初始请求的主机名不一致(跨域跳转)时,自动删除敏感请求头,防止凭据泄露给重定向目标:
| 请求头 | 目标主机 = 初始请求主机 | 目标主机 ≠ 初始请求主机 |
|---|---|---|
Authorization | 保留 | 删除 |
Proxy-Authorization | 保留 | 删除 |
Cookie | 保留 | 删除 |
| 其他自定义头 | 保留 | 保留 |
判定细节:
- 比较基准是初始请求的主机名(第一跳的
via[0]),不是上一跳。api.example.com → www.example.com属于跨域(精确主机名比较,子域不同也算);A → B → A回到初始主机则不剥离。 - 剥离独立于 cookie jar:即使未启用
EnableCookies,手工设置的Cookie请求头在跨域跳转时同样被删除。
配合 WithBasicAuth / WithBearerToken 使用时无需额外处理——令牌只发往原始主机,不会随跳转带给第三方域。
环形重定向检测
除次数上限外,引擎还检测环形重定向(跳转目标此前已在链路中出现过)。命中时请求立即以错误结束,不必等次数用尽:
A → B → A 环形:报错 circular redirect detected: A
A → A → A 连续同 URL:不算环形(服务器可能每次返回不同响应)环形检测与 MaxRedirects 次数上限互为补充:次数上限兜住一切循环,环形检测把「明显绕圈」的链路提前识破,减少无谓的请求。
重定向错误归类
跟随被拒绝(超限、环形、白名单、SSRF 拦截)时,请求以 ClientError 结束,Type 统一为 ErrorTypeValidation:
| 底层错误信息 | Message 字段 | 触发条件 |
|---|---|---|
stopped after N redirects | redirect limit exceeded | 跳转次数达到 MaxRedirects(或默认 10) |
circular redirect detected: <URL> | circular redirect detected | 目标 URL 早已出现在跳转链中 |
redirect blocked by whitelist: ... | redirect blocked by policy | 目标域名不在 RedirectWhitelist 内 |
redirect blocked: ... | redirect blocked by policy | 目标主机被 SSRF 防护拦截 |
package main
import (
"errors"
"fmt"
"log"
"github.com/cybergodev/httpc"
)
func main() {
cfg := httpc.DefaultConfig()
cfg.Defaults.MaxRedirects = 2 // 只允许跟随 2 次
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// /redirect/5 需要跟随 5 次,必然超过上限
_, err = client.Get("https://httpbin.org/redirect/5")
if err != nil {
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) && clientErr.Type == httpc.ErrorTypeValidation {
fmt.Println("重定向被拒绝:", clientErr.Message)
// 输出:重定向被拒绝: redirect limit exceeded
}
}
}重定向目标的 SSRF 校验还包含两条硬性规则:只允许 http/https 协议(ftp://、自定义协议的 Location 被拒绝)与目标主机不得为空。校验阶段不做 DNS 解析——完整的 IP 验证与 DNS rebinding 防护在连接建立时由拨号器执行(详见 SSRF 防护)。
域名白名单
Security.RedirectWhitelist 将重定向目标限制在受信域名内,防御开放重定向攻击:
cfg := httpc.DefaultConfig()
cfg.Security.RedirectWhitelist = []string{
"api.example.com",
"*.cdn.example.com", // 通配符:匹配严格子域,不含裸域
}
client, err := httpc.New(cfg)匹配规则细节:
- 精确匹配:
api.example.com只匹配自身。 - 通配符:
*.cdn.example.com匹配严格子域(如img.cdn.example.com),不匹配裸域cdn.example.com;两者都要放行需同时列出。 - 比较对象是
Location目标的主机名(不含端口与协议),比较前做归一化:忽略首尾空白、大小写不敏感。
设置后,重定向到白名单之外域名的请求会被拒绝;重定向目标同时经过 SSRF IP 验证。处理用户提供的 URL 时,建议配合 SecureConfig 或白名单使用。
手动处理重定向
需要逐跳检查、按条件跟随或自定义日志时,禁止自动跟随并循环处理。注意两点:Location 可能是相对地址,需按当前 URL 解析成绝对地址;手动循环不经过白名单检查,但每一跳建连时连接层的 SSRF IP 校验照常生效。
package main
import (
"fmt"
"log"
"net/url"
"github.com/cybergodev/httpc"
)
func main() {
cfg := httpc.DefaultConfig()
cfg.Defaults.FollowRedirects = false
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer client.Close()
currentURL := "https://httpbin.org/redirect/3"
base, err := url.Parse(currentURL)
if err != nil {
log.Fatal(err)
}
for i := 0; i < 5; i++ {
result, err := client.Get(currentURL)
if err != nil {
log.Fatal(err)
}
if !result.IsRedirect() {
fmt.Println("到达最终地址:", currentURL)
break
}
location := result.Response.Headers.Get("Location")
if location == "" {
fmt.Println("重定向响应缺少 Location 头,停止跟随")
break
}
// 相对地址按当前 URL 解析为绝对地址
next, err := base.Parse(location)
if err != nil {
log.Fatal(err)
}
fmt.Printf("第 %d 跳:%s\n", i+1, next.String())
currentURL = next.String()
base = next
}
}手动循环中的安全责任
自动跟随时的白名单与重定向目标预检不会作用于手动循环。处理不可信来源的 Location 时,建议在循环里自行校验目标域名(或复用白名单逻辑);连接层的 SSRF 校验仍然兜底,但域级控制需要自己做。
常见问题
| 现象 | 原因 | 解决方案 |
|---|---|---|
| POST 收到 307/308 未跟随 | 请求体不可重放(未设置 GetBody),按规范不自动跟随 | 手动循环处理,或让服务端改用 302/303 |
WithMaxRedirects(0) 没有禁用跟随 | 0 是「未设置」哨兵值,回退默认 10 | 用 WithFollowRedirects(false) |
带 Location 的 300/399 没有跳转 | 引擎只跟随 301/302/303/307/308 | 用 IsRedirect() + Location 自行处理 |
| 跳转后查询参数「丢失」 | 目标查询串完全由 Location 决定,客户端不合并原请求参数 | 服务端在 Location 中带上需要的参数 |
| 想知道最终落在哪个域名 | RedirectChain 不含最终 URL | 读 result.Request.URL |
| 跨域跳转后请求 401 | Authorization/Cookie 被跨域剥离(安全设计) | 对新域名重新认证,或让服务端同域跳转 |