资讯详情

go net/http.Client 处理redirect

现象

源码

原因

解决

参考


现象

A服务通过post要求访问B服务时,B服务返回301,让A服务访问C服务。在此期间method没有改变,只是修改了location。 但当A服务通过301访问C服务时,method竟然由post变成了get请求。

源码

net/http/client.go

 func (c *Client) do(req *Request) (retres *Response, reterr error) {  if testHookClientDoResult != nil {   defer func() { testHookClientDoResult(retres, reterr) }()  }  if req.URL == nil {   req.closeBody()   return nil, &url.Error{    Op:  urlErrorOp(req.Method),    Err: errors.New("http: nil Request.URL"),   }  }   var (   deadline      = c.deadline()   reqs          []*Request   resp          *Response   copyHeaders   = c.makeHeadersCopier(req)   reqBodyClosed = false // have we closed the current req.Body?    // Redirect behavior:   redirectMethod string   includeBody    bool  )  uerr := func(err error) error {   // the body may have been closed already by c.send()   if !reqBodyClosed {    req.closeBody()   }   var urlStr string   if resp !reqBodyClosed {    req.closeBody()   }   var urlStr string   if resp != nil && resp.Request != nil {    urlStr = stripPassword(resp.Request.URL)   } else {    urlStr = stripPassword(req.URL)   }   return &url.Error{    Op:  urlErrorOp(reqs[0].Method),    URL: urlStr,    Err: err,   }  }  for {   // For all but the first request, create the next   // request hop and replace req.   if len(reqs) > 0 {    loc := resp.Header.Get("Location")    if loc == "" {     resp.closeBody()     return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))    }    u, err := req.URL.Parse(loc)    if err != nil {     resp.closeBody()     return nil, uerr(fmt.Errorf("failed to parse Location header %q: %v", loc, err))    }    host := ""    if req.Host != "" && req.Host != req.URL.Host {     // If the caller specified a custom Host header and the     // redirect location is relative, preserve the Host header     // through the redirect. See issue #22233.     if u, _ := url.Parse(loc); u != nil && !u.IsAbs() {      host = req.Host     }    }    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()     if err != nil {      resp.closeBody()      return nil, uerr(err)     }     req.ContentLength = ireq.ContentLength    }     // Copy original headers before setting the Referer,    // in case the user set Referer on their first request.    // If they really want to override, they can do it in    // their CheckRedirect func.    copyHeaders(req)     // Add the Referer header from the most recent    // request URL to the new one, if it's not https->http:    if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {     req.Header.Set("Referer", ref)    }    err = c.checkRedirect(req, reqs)     // Sentinel error to let users select the    // previous response, without closing its    // body. See Issue 10069.    if err == ErrUseLastResponse {     return resp, nil    }     // Close the previous response's body. But    // read at least some of the body so if it's    // small the underlying TCP connection will be    // re-used. No need to check for errors: if it    // fails, the Transport won't reuse it anyway.    const maxBodySlurpSize = 2 << 10    if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {     io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)    }    resp.Body.Close()     if err != nil {     // Special case for Go 1 compatibility: return both the response     // and an error if the CheckRedirect function failed.     // See https://golang.org/issue/3795     // The resp.Body has already been closed.     ue := uerr(err)     ue.(*url.Error).URL = loc     return resp, ue    }   }    reqs = append(reqs, req)   var err error   var didTimeout func() bool   if resp, didTimeout, err = c.send(req, deadline); err != nil {    // c.send() always closes req.Body    reqBodyClosed = true    if !deadline.IsZero() && didTimeout() {     err = &httpError{      // TODO: early in cycle: s/Client.Timeout exceeded/timeout or context cancellation/      err:     err.Error()   " (Client.Timeout exceeded while awaiting headers)",      timeout: true,     }    }    retrn nil, uerr(err)
		}
            
		var shouldRedirect bool
        // 处理redirect请求
		redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
		if !shouldRedirect {
			return resp, nil
		}

		req.closeBody()
	}
}


// 处理redirect请求
// redirectBehavior describes what should happen when the
// client encounters a 3xx status code from the server
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

		// RFC 2616 allowed automatic redirection only with GET and
		// HEAD requests. RFC 7231 lifts this restriction, but we still
		// restrict other methods to GET to maintain compatibility.
		// See Issue 18570.
		if reqMethod != "GET" && reqMethod != "HEAD" {
			redirectMethod = "GET"
		}
	case 307, 308:
		redirectMethod = reqMethod
		shouldRedirect = true
		includeBody = true

		// Treat 307 and 308 specially, since they're new in
		// Go 1.8, and they also require re-sending the request body.
		if resp.Header.Get("Location") == "" {
			// 308s have been observed in the wild being served
			// without Location headers. Since Go 1.7 and earlier
			// didn't follow these codes, just stop here instead
			// of returning an error.
			// See Issue 17773.
			shouldRedirect = false
			break
		}
		if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
			// We had a request body, and 307/308 require
			// re-sending it, but GetBody is not defined. So just
			// return this response to the user instead of an
			// error, like we did in Go 1.7 and earlier.
			shouldRedirect = false
		}
	}
	return redirectMethod, shouldRedirect, includeBody
}

原因

根据源码:

redirectBehavior(reqMethod string, resp *Response, ireq *Request)

可知,当状态码为301,302,303的时候,会把请求method变为get,当状态码为307,308的时候回维持原来的method不变。

解决

B服务不再返回301,返回307或者308

参考

https://zhuanlan.zhihu.com/p/23811184

https://studygolang.com/articles/5774

 

标签: 308s传感器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司