Go HTTP Redirect的知识点总结

HTTP 规范中定义了返回码为 3xx 代表客户端需要做一些额外的工作来完成请求,大部分3xx用来做转发(redirect)。

状态码的详细说明可以参照规范或者 wikipedia维基百科, 以下是代码的简短介绍。

  • 300 Multiple Choices: 返回多个可供选择的资源
  • 301 Moved Permanently: 请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一
  • 302 Found: 请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求,HTTP 1.0中的意义是Moved Temporarily,但是很多浏览器的实现是按照303的处实现的,所以HTTP 1.1中增加了 303和307的状态码来区分不同的行为
  • 303 See Other (since HTTP/1.1): 对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用GET的方式访问那个资源
  • 304 Not Modified (RFC 7232): 请求的资源没有改变
  • 305 Use Proxy (since HTTP/1.1): 被请求的资源必须通过指定的代理才能被访问
  • 306 Switch Proxy: 在最新版的规范中,306状态码已经不再被使用
  • 307 Temporary Redirect (since HTTP/1.1): 请求的资源现在临时从不同的URI响应请求,和303不同,它还是使用原先的Method
  • 308 Permanent Redirect (RFC 7538): 请求的资源已永久移动到新位置,并且新请求的Method不能改变

Go 的 http 库在实现的过程中也在不断的完成和修改其中的Bug,在 1.8版本中解决了前面版本中实现的问题 (你可以在 Go issues中搜索 redirect 来查看相关的issue)。 本文梳理了 Go 中 Redirect 的相关知识,以便你在遇到转发的问题时心中有数。

转发策略和默认转发次数。

http.Client包含一个CheckRedirect字段,用来定义转发的策略,如果你没有设置,则默认使用defaultCheckRedirect:

1
2
3
4
5
6
7
func (c *Client) checkRedirect(req *Request, via []*Request) error {
fn := c.CheckRedirect
if fn == nil {
fn = defaultCheckRedirect
}
return fn(req, via)
}

这个函数会在执行转发之前被调用,可以看到这个函数如果返回err,则不再进行转发了。
这个函数的第一个参数req是即将转发使用的request,第二个参数 via已经请求的requests。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for {
……
//需要转发
{
……
err = c.checkRedirect(req, reqs)
if err == ErrUseLastResponse {
return resp, nil
}
//discard previous response
……
if err != nil {
ue := uerr(err)
ue.(*url.Error).URL = loc
return resp, ue
}
}
……
}

默认的转发策略是最多10次转发, 避免转发次数过高或者死循环。

1
2
3
4
5
6
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}

如果你要实现不同的转发策略,你需要定义自己的CheckRedirect

转发安全

1)当转发的request中包含安全的信息Header时, 比如AuthorizationWWW-AuthenticateCookie等Header,如果是跨域,则这些头部不会被复制到新的请求中。

1
2
3
4
5
6
7
8
9
func shouldCopyHeaderOnRedirect(headerKey string, initial, dest *url.URL) bool {
switch CanonicalHeaderKey(headerKey) {
case "Authorization", "Www-Authenticate", "Cookie", "Cookie2":
ihost := strings.ToLower(initial.Host)
dhost := strings.ToLower(dest.Host)
return isDomainOrSubdomain(dhost, ihost)
}
return true
}

2)如果设置了一个非空的 cookie Jar, 转发响应会修改 cookie jar中的值,但是下一次转发的时候, Cookieheader会被处理,忽略那些变动的cookie.

when forwarding the "Cookie" header with a non-nil cookie Jar.
Since each redirect may mutate the state of the cookie jar,
a redirect may possibly alter a cookie set in the initial request.
When forwarding the "Cookie" header, any mutated cookies will be omitted,
with the expectation that the Jar will insert those mutated cookies
with the updated values (assuming the origin matches).
If Jar is nil, the initial cookies are forwarded without change.

具体的你可以查看 makeHeadersCopier的实现。
可以看到每次redirect会删除上次的Redirect造成的变动,再恢复原始的请求的Coookie。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if c.Jar != nil && icookies != nil {
var changed bool
resp := req.Response // The response that caused the upcoming redirect
for _, c := range resp.Cookies() {
if _, ok := icookies[c.Name]; ok {
delete(icookies, c.Name)
changed = true
}
}
if changed {
ireqhdr.Del("Cookie")
var ss []string
for _, cs := range icookies {
for _, c := range cs {
ss = append(ss, c.Name+"="+c.Value)
}
}
sort.Strings(ss) // Ensure deterministic headers
ireqhdr.Set("Cookie", strings.Join(ss, "; "))
}
}
// Copy the initial request's Header values
// (at least the safe ones).
for k, vv := range ireqhdr {
if shouldCopyHeaderOnRedirect(k, preq.URL, req.URL) {
req.Header[k] = vv
}
}

转发规则

当服务器返回一个转发response的时候, client首先使用CheckRedirect函数检查是否要进行转发。

默认会处理下列请求:

  • 301 (Moved Permanently)
  • 302 (Found)
  • 303 (See Other)
  • 307 (Temporary Redirect)
  • 308 (Permanent Redirect)

如果需要转发, 对于301302303的状态码, 接下来转发的请求会将请求Method转换成GET method (如果原始请求Method是HEAD则不变,还是HEAD), 而且body为空, 尽管原始的请求可能包含body。 对于307308状态码,接下来转发的请求的Method没有变化,和原始的请求保持一致, 并且还是使用原始的body内容来发送转发请求。

代码处理的逻辑是由redirectBehavior函数实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
if reqMethod != "GET" && reqMethod != "HEAD" {
redirectMethod = "GET"
}
case 307, 308:
redirectMethod = reqMethod
shouldRedirect = true
includeBody = true
if resp.Header.Get("Location") == "" {
shouldRedirect = false
break
}
if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
shouldRedirect = false
}
}
return redirectMethod, shouldRedirect, includeBody
}

以上是介绍的http Redirect相关的内容,它们主要是客户端的代码逻辑。 下面两节介绍一下与http redirect有一点点关系的内容。

RedirectHandler

http定义了一个便利类型: RedirectHandler, 它是一个预定义的http handler,可以将指定的请求转发到指定的url中, 主要就是使用设定的 url 和 status code 设置 response 的 Location。

它用作服务器端的开发中。

1
func RedirectHandler(url string, code int) Handler

注意 code应该是 3xx的状态码, 通常是StatusMovedPermanently, StatusFound 或者 StatusSeeOther

RoundTripper

RoundTripper也用作客户端的开发。

RoundTripper代表一次http 的请求。 当使用redirect的时候,你可能redirect多次,也就是执行了n次的http请求。

1
2
3
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}

DefaultTransport是默认RoundTripper的实现。它建立网络连接,并且会缓存以便后续的请求重用。它会使用$HTTP_PROXY$NO_PROXY (或者 $http_proxy, $no_proxy)环境变量来设置代理.

其它的RoundTripper的实现还有NewFileTransport创建的RoundTripper,用来服务文件系统,将文件系统映射成http的处理方式。

1
2
3
4
5
6
7
8
9
10
11
12
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

Redirect的时候,默认转发是http库自动帮你实现的,如果你想对于转发过程做深入的跟踪的话(简单跟踪可以使用httptrace),你可以自定义一个RoundTripper,比如下面的这个:

http://stackoverflow.com/questions/24577494/how-to-get-the-http-redirect-status-codes-in-golang
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type LogRedirects struct {
Transport http.RoundTripper
}
func (l LogRedirects) RoundTrip(req *http.Request) (resp *http.Response, err error) {
t := l.Transport
if t == nil {
t = http.DefaultTransport
}
resp, err = t.RoundTrip(req)
if err != nil {
return
}
switch resp.StatusCode {
case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect:
log.Println("Request for", req.URL, "redirected with status", resp.StatusCode)
}
return
}

创建Client的时候可以使用下面的代码:

1
client := &http.Client{Transport: LogRedirects{}}

这样每次转发过程我们都可以追踪。