這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
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
:
1234567 |
func (c *Client) checkRedirect(req *Request, via []*Request) error {fn := c.CheckRedirectif fn == nil {fn = defaultCheckRedirect}return fn(req, via)} |
這個函數會在執行轉寄之前被調用,可以看到這個函數如果返回err,則不再進行轉寄了。
這個函數的第一個參數req
是即將轉寄使用的request,第二個參數 via
已經請求的requests。
12345678910111213141516171819202122 |
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 = locreturn resp, ue} } ……} |
預設的轉寄策略是最多10次轉寄, 避免轉寄次數過高或者死迴圈。
123456 |
func defaultCheckRedirect(req *Request, via []*Request) error {if len(via) >= 10 {return errors.New("stopped after 10 redirects")}return nil} |
如果你要實現不同的轉寄策略,你需要定義自己的CheckRedirect
。
轉寄安全
1)當轉寄的request中包含安全的資訊Header時, 比如Authorization
、WWW-Authenticate
、Cookie
等Header,如果是跨域,則這些頭部不會被複製到新的請求中。
123456789 |
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中的值,但是下一次轉寄的時候, Cookie
header會被處理,忽略那些變動的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。
1234567891011121314151617181920212223242526272829 |
if c.Jar != nil && icookies != nil {var changed boolresp := req.Response // The response that caused the upcoming redirectfor _, c := range resp.Cookies() {if _, ok := icookies[c.Name]; ok {delete(icookies, c.Name)changed = true}}if changed {ireqhdr.Del("Cookie")var ss []stringfor _, cs := range icookies {for _, c := range cs {ss = append(ss, c.Name+"="+c.Value)}}sort.Strings(ss) // Ensure deterministic headersireqhdr.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)
如果需要轉寄, 對於301
、302
、303
的狀態代碼, 接下來轉寄的請求會將請求Method轉換成GET
method (如果原始請求Method是HEAD
則不變,還是HEAD
), 而且body為空白, 儘管原始的請求可能包含body。 對於307
、308
狀態代碼,接下來轉寄的請求的Method沒有變化,和原始的請求保持一致, 並且還是使用原始的body內容來發送轉寄請求。
代碼處理的邏輯是由redirectBehavior
函數實現的。
1234567891011121314151617181920212223242526 |
func redirectBehavior(reqMethod string, resp *Response, ireq *Request) (redirectMethod string, shouldRedirect, includeBody bool) {switch resp.StatusCode {case 301, 302, 303:redirectMethod = reqMethodshouldRedirect = trueincludeBody = falseif reqMethod != "GET" && reqMethod != "HEAD" {redirectMethod = "GET"}case 307, 308:redirectMethod = reqMethodshouldRedirect = trueincludeBody = trueif resp.Header.Get("Location") == "" {shouldRedirect = falsebreak}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請求。
123 |
type RoundTripper interface { RoundTrip(*Request) (*Response, error)} |
DefaultTransport
是預設RoundTripper的實現。它建立網路連接,並且會緩衝以便後續的請求重用。它會使用$HTTP_PROXY
和$NO_PROXY
(或者 $http_proxy, $no_proxy)環境變數來設定代理.
其它的RoundTripper的實現還有NewFileTransport
建立的RoundTripper,用來服務檔案系統,將檔案系統映射成http的處理方式。
123456789101112 |
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
12345678910111213141516171819 |
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{}} |
這樣每次轉寄過程我們都可以追蹤。