分析http.Client
源碼實現的原由, 是由於在使用以下步驟模擬網站登陸時, 出現了問題, 參考知乎 - go net/http.Client 處理redirect:前端
POST
帳號密碼等參數進行登陸git
下發token
, 此token
經過cookie
下發github
重定向到主頁/
golang
在經過http.Post
進行請求, 預期不進行重定向, 可以直接獲取到cookie
值, 但實際上go幫咱們處理了重定向, 丟失了cookie
值數組
分析源碼後, 能夠很輕易地解決這個問題:瀏覽器
// 請求http.calabash.top將被301重定向到https
myClient := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
respWithNoRedirect, _ := myClient.Get("http://blog.calabash.top")
respWithRedirect, _ := http.Get("http://blog.calabash.top")
fmt.Println(respWithNoRedirect.StatusCode) // 301
fmt.Println(respWithRedirect.StatusCode) // 200
複製代碼
HTTP客戶端, 其零值爲DefaultClient
, 本處分析的核心在於處理重定向的方式bash
type Client struct {
// 分析範圍以外
Transport RoundTripper
// 重定向策略
CheckRedirect func(req *Request, via []*Request) error
// Cookie的存儲, 單純的get/set方法
Jar CookieJar
// 超時
Timeout time.Duration
}
var DefaultClient = &Client{}
複製代碼
當碰見重定向時, 除了如下的狀況, Client
將轉發全部初始請求頭:cookie
當重定向地址和初始地址的domain
不一樣且也不是sub domain
時, 請求頭中的cookie
, authorization
等敏感字段將被忽略閉包
重定向可能會改變初始請求中的cookie
值, 所以轉發cookie
請求頭時將忽略任何有變化的cookie
app
redirectBehavior
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
}
複製代碼
由源碼能夠獲得以下的信息:
對於301, 302, 303的狀態碼, 重定向不會附帶請求體, 而且對於非GET/HEAD
請求方法會被強制修改成GET
請求方法
對於307, 308狀態碼, 重定向不會更改修改請求方法且會重用請求體, Location
爲空或Body
爲空時, 不該重定向
參考MDN - Http Status Code, 其中有這樣的描述
301: 儘管標準要求瀏覽器在收到該響應並進行重定向時不該該修改http method和body,可是有一些瀏覽器可能會有問題。 因此最好是在應對GET或HEAD方法時使用301,其餘狀況使用308來替代301
302: 即便規範要求瀏覽器在重定向時保證請求方法和請求主體不變,但並非全部的用戶代理都會遵循這一點 因此推薦僅在響應GET或HEAD方法時採用302狀態碼,其餘狀況使用307來替代301
303: 一般做爲PUT或POST操做的返回結果,它表示重定向連接指向的不是新上傳的資源,而是另一個頁面 而請求重定向頁面的方法要老是使用GET
307: 狀態碼307與302之間的惟一區別在於,當發送重定向請求的時候,307狀態碼能夠確保請求方法和消息主體不會發生變化
308: 在重定向過程當中,請求方法和消息主體不會發生改變,然而在返回301狀態碼的狀況下,請求方法有時候會被客戶端錯誤地修改成GET方法。
可以發現MDN
的描述和redirectBehavior
源碼的設計很是吻合
checkRedirect
當沒有指定CheckRedirect
函數時, Client
會使用defaultCheckRedirect
策略: 默認最多重定向十次
func (c *Client) checkRedirect(req *Request, via []*Request) error {
fn := c.CheckRedirect
if fn == nil {
fn = defaultCheckRedirect
}
return fn(req, via)
}
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
複製代碼
makeHeadersCopier
這個方法經過閉包的方式, 完成了上面提到的對請求頭的處理:
當重定向地址和初始地址的域名不一樣且也不是子域時, 請求頭中的cookie
, authorization
等敏感字段將被忽略
重定向可能會改變初始請求中的cookie
值, 所以轉發cookie
請求頭時將忽略任何有變化的cookie
func (c *Client) makeHeadersCopier(ireq *Request) func(*Request) {
// 克隆一份header
var (
ireqhdr = ireq.Header.Clone()
icookies map[string][]*Cookie
)
// 再維護一個cookie哈希表
if c.Jar != nil && ireq.Header.Get("Cookie") != "" {
icookies = make(map[string][]*Cookie)
for _, c := range ireq.Cookies() {
icookies[c.Name] = append(icookies[c.Name], c)
}
}
// The previous request
preq := ireq
// 調用返回一個接受req的函數, 用於對拷貝header進行處理
return func(req *Request) {
if c.Jar != nil && icookies != nil {
var changed bool
resp := req.Response
// 若是響應中的set-cookie操做設定的cookie名稱存在於cookie哈希表中, 從哈希表中刪除它
for _, c := range resp.Cookies() {
if _, ok := icookies[c.Name]; ok {
delete(icookies, c.Name)
changed = true
}
}
// 忽略全部變化的cookie, 從新組裝cookie請求頭字段
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, "; "))
}
}
for k, vv := range ireqhdr {
// 對於非同域或子域, 敏感請求頭的處理
if shouldCopyHeaderOnRedirect(k, preq.URL, req.URL) {
req.Header[k] = vv
}
}
// Update previous Request with the current request
preq = req
}
}
複製代碼
順便貼一下對於非同域或子域, 敏感請求頭的處理, 比較簡單易懂
func shouldCopyHeaderOnRedirect(headerKey string, initial, dest *url.URL) bool {
switch CanonicalHeaderKey(headerKey) {
case "Authorization", "Www-Authenticate", "Cookie", "Cookie2":
ihost := canonicalAddr(initial)
dhost := canonicalAddr(dest)
return isDomainOrSubdomain(dhost, ihost)
}
// All other headers are copied:
return true
}
func isDomainOrSubdomain(sub, parent string) bool {
if sub == parent {
return true
}
if !strings.HasSuffix(sub, parent) {
return false
}
return sub[len(sub)-len(parent)-1] == '.'
}
複製代碼
http.Get
等方法的背後從源碼中能夠找到, http.Get
等方法都是DefaultClient
包裝後的一層
var DefaultClient = &Client{}
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
return DefaultClient.Post(url, contentType, body)
}
func PostForm(url string, data url.Values) (resp *Response, err error) {
return DefaultClient.PostForm(url, data)
}
func Head(url string) (resp *Response, err error) {
return DefaultClient.Head(url)
}
複製代碼
而這些方法最終調用的都是Client.Do
方法, 這部分能夠說十分開胃了
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
req, err := NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.Do(req)
}
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error) {
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}
func (c *Client) Head(url string) (resp *Response, err error) {
req, err := NewRequest("HEAD", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
複製代碼
Client.Do
方法func (c *Client) Do(req *Request) (*Response, error) {
return c.do(req)
}
複製代碼
開始分析Client.do
方法, 該方法大概200行, 爲了不對流程分析的影響, 將去除錯誤處理部分代碼, 簡化代碼以下
func (c *Client) do(req *Request) (retres *Response, reterr error) {
var (
deadline = c.deadline()
reqs []*Request
resp *Response
copyHeaders = c.makeHeadersCopier(req)
reqBodyClosed = false
redirectMethod string
includeBody bool
)
for {
if len(reqs) > 0 {
loc := resp.Header.Get("Location")
ireq := reqs[0]
req = &Request{
Method: redirectMethod,
Response: resp,
URL: u,
Header: make(Header),
Host: host,
Cancel: ireq.Cancel,
ctx: ireq.ctx,
}
if includeBody && ireq.GetBody != nil {
req.Body, err = ireq.GetBody()
req.ContentLength = ireq.ContentLength
}
copyHeaders(req)
err = c.checkRedirect(req, reqs)
if err == ErrUseLastResponse {
return resp, nil
}
resp.Body.Close()
}
reqs = append(reqs, req)
var err error
var didTimeout func() bool
resp, didTimeout, err = c.send(req, deadline)
var shouldRedirect bool
redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
if !shouldRedirect {
return resp, nil
}
req.closeBody()
}
}
複製代碼
拷貝req
中的所有headers
, 返回一個函數copyHeaders
, 出現重定向時根據規則處理一部分請求頭字段
reqs = append(reqs, req)
, 將請求記錄到reqs
數組中, 第一個req
必定是最原始的請求, 後面的req
必定都是重定向的~
resp = c.send(req)
, 正兒八經發起請求, 獲得resp
redirectBehavior(req.Method, resp, reqs[0])
, 根據響應判斷是否須要重定向, 若是不須要, 流程結束, 若是須要, 繼續向下
進入if len(reqs) > 0
分支, 開始對重定向的處理
從resp
中獲取Location
字段, 結合原始請求reqs[0]
組裝出新的重定向請求, 並賦值給req
copyHeaders(req)
, 比對reqs[0]
和req
, 根據上面提到的兩條規則去除特定的字段
c.checkRedirect(req, reqs)
判斷是否符合重定向策略, 若是不符合, 返回最後一個resp, 再也不繼續重定向
再次執行步驟1-4
雖然是個前端, 第一次看Go源碼, 體驗仍是很是爽的, 800行代碼, 400行註釋, 量也不是很大QAQ。
總結一下經驗:
帶着問題看源碼, 便於肯定方向
沿着主分支分析, 去除/跳過一些旁枝末節和錯誤處理的代碼
DEBUG是能最快肯定流程的方式