现象
源码
原因
解决
参考
现象
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