Commit aecfcd82 authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: clean up the Client redirect code, document Body.Close rules more

Issue #8633 (and #9134) noted that we didn't document the rules about
closing the Response.Body when Client.Do returned both a non-nil
*Response and a non-nil error (which can only happen when the user's
CheckRedirect returns an error).

In the process of investigating, I cleaned this code up a bunch, but
no user-visible behavior should have changed, except perhaps some
better error messages in some cases.

It turns out it's always been the case that when a CheckRedirect error
occurs, the Response.Body is already closed. Document that.

And the new code makes that more obvious too.

Fixes #8633

Change-Id: Ibc40cc786ad7fc4e0cf470d66bb559c3b931684d
Reviewed-on: https://go-review.googlesource.com/21364
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: 's avatarAndrew Gerrand <adg@golang.org>
parent 92bb694a
...@@ -44,9 +44,9 @@ type Client struct { ...@@ -44,9 +44,9 @@ type Client struct {
// following an HTTP redirect. The arguments req and via are // following an HTTP redirect. The arguments req and via are
// the upcoming request and the requests made already, oldest // the upcoming request and the requests made already, oldest
// first. If CheckRedirect returns an error, the Client's Get // first. If CheckRedirect returns an error, the Client's Get
// method returns both the previous Response and // method returns both the previous Response (with its Body
// CheckRedirect's error (wrapped in a url.Error) instead of // closed) and CheckRedirect's error (wrapped in a url.Error)
// issuing the Request req. // instead of issuing the Request req.
// //
// If CheckRedirect is nil, the Client uses its default policy, // If CheckRedirect is nil, the Client uses its default policy,
// which is to stop after 10 consecutive requests. // which is to stop after 10 consecutive requests.
...@@ -153,28 +153,33 @@ func (c *Client) send(req *Request, deadline time.Time) (*Response, error) { ...@@ -153,28 +153,33 @@ func (c *Client) send(req *Request, deadline time.Time) (*Response, error) {
c.Jar.SetCookies(req.URL, rc) c.Jar.SetCookies(req.URL, rc)
} }
} }
return resp, err return resp, nil
} }
// Do sends an HTTP request and returns an HTTP response, following // Do sends an HTTP request and returns an HTTP response, following
// policy (e.g. redirects, cookies, auth) as configured on the client. // policy (such as redirects, cookies, auth) as configured on the
// client.
// //
// An error is returned if caused by client policy (such as // An error is returned if caused by client policy (such as
// CheckRedirect), or if there was an HTTP protocol error. // CheckRedirect), or failure to speak HTTP (such as a network
// A non-2xx response doesn't cause an error. // connectivity problem). A non-2xx status code doesn't cause an
// // error.
// When err is nil, resp always contains a non-nil resp.Body.
// //
// Callers should close resp.Body when done reading from it. If // If the returned error is nil, the Response will contain a non-nil
// resp.Body is not closed, the Client's underlying RoundTripper // Body which the user is expected to close. If the Body is not
// (typically Transport) may not be able to re-use a persistent TCP // closed, the Client's underlying RoundTripper (typically Transport)
// connection to the server for a subsequent "keep-alive" request. // may not be able to re-use a persistent TCP connection to the server
// for a subsequent "keep-alive" request.
// //
// The request Body, if non-nil, will be closed by the underlying // The request Body, if non-nil, will be closed by the underlying
// Transport, even on errors. // Transport, even on errors.
// //
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when CheckRedirect fails, and even then
// the returned Response.Body is already closed.
//
// Generally Get, Post, or PostForm will be used instead of Do. // Generally Get, Post, or PostForm will be used instead of Do.
func (c *Client) Do(req *Request) (resp *Response, err error) { func (c *Client) Do(req *Request) (*Response, error) {
method := valueOrDefault(req.Method, "GET") method := valueOrDefault(req.Method, "GET")
if method == "GET" || method == "HEAD" { if method == "GET" || method == "HEAD" {
return c.doFollowingRedirects(req, shouldRedirectGet) return c.doFollowingRedirects(req, shouldRedirectGet)
...@@ -416,54 +421,83 @@ func (c *Client) Get(url string) (resp *Response, err error) { ...@@ -416,54 +421,83 @@ func (c *Client) Get(url string) (resp *Response, err error) {
func alwaysFalse() bool { return false } func alwaysFalse() bool { return false }
func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error) { // checkRedirect calls either the user's configured CheckRedirect
var base *url.URL // function, or the default.
redirectChecker := c.CheckRedirect func (c *Client) checkRedirect(req *Request, via []*Request) error {
if redirectChecker == nil { fn := c.CheckRedirect
redirectChecker = defaultCheckRedirect if fn == nil {
fn = defaultCheckRedirect
} }
var via []*Request return fn(req, via)
}
if ireq.URL == nil { func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) bool) (*Response, error) {
ireq.closeBody() if req.URL == nil {
req.closeBody()
return nil, errors.New("http: nil Request.URL") return nil, errors.New("http: nil Request.URL")
} }
req := ireq var (
deadline := c.deadline() deadline = c.deadline()
reqs []*Request
urlStr := "" // next relative or absolute URL to fetch (after first request) resp *Response
redirectFailed := false )
for redirect := 0; ; redirect++ { uerr := func(err error) error {
if redirect != 0 { req.closeBody()
nreq := new(Request) method := valueOrDefault(reqs[0].Method, "GET")
nreq.Cancel = ireq.Cancel var urlStr string
nreq.Method = ireq.Method if resp != nil {
if ireq.Method == "POST" || ireq.Method == "PUT" { urlStr = resp.Request.URL.String()
nreq.Method = "GET" } else {
urlStr = req.URL.String()
}
return &url.Error{
Op: method[:1] + strings.ToLower(method[1:]),
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 == "" {
return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))
} }
nreq.Header = make(Header) u, err := req.URL.Parse(loc)
nreq.URL, err = base.Parse(urlStr)
if err != nil { if err != nil {
break return nil, uerr(fmt.Errorf("failed to parse Location header %q: %v", loc, err))
} }
if len(via) > 0 { ireq := reqs[0]
// Add the Referer header. req = &Request{
lastReq := via[len(via)-1] Method: ireq.Method,
if ref := refererForURL(lastReq.URL, nreq.URL); ref != "" { URL: u,
nreq.Header.Set("Referer", ref) Header: make(Header),
} Cancel: ireq.Cancel,
}
err = redirectChecker(nreq, via) if ireq.Method == "POST" || ireq.Method == "PUT" {
if err != nil { req.Method = "GET"
redirectFailed = true }
break // 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)
}
if err := c.checkRedirect(req, reqs); 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
} }
req = nreq
} }
urlStr = req.URL.String() reqs = append(reqs, req)
var err error
if resp, err = c.send(req, deadline); err != nil { if resp, err = c.send(req, deadline); err != nil {
if !deadline.IsZero() && !time.Now().Before(deadline) { if !deadline.IsZero() && !time.Now().Before(deadline) {
err = &httpError{ err = &httpError{
...@@ -471,46 +505,21 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo ...@@ -471,46 +505,21 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo
timeout: true, timeout: true,
} }
} }
break return nil, uerr(err)
} }
if shouldRedirect(resp.StatusCode) { if !shouldRedirect(resp.StatusCode) {
// Read the body if small so underlying TCP connection will be re-used. return resp, nil
// No need to check for errors: if it fails, 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 urlStr = resp.Header.Get("Location"); urlStr == "" {
err = fmt.Errorf("%d response missing Location header", resp.StatusCode)
break
}
base = req.URL
via = append(via, req)
continue
} }
return resp, nil
}
method := valueOrDefault(ireq.Method, "GET")
urlErr := &url.Error{
Op: method[:1] + strings.ToLower(method[1:]),
URL: urlStr,
Err: err,
}
if redirectFailed { // Read the body if small so underlying TCP connection will be re-used.
// Special case for Go 1 compatibility: return both the response // No need to check for errors: if it fails, Transport won't reuse it anyway.
// and an error if the CheckRedirect function failed. const maxBodySlurpSize = 2 << 10
// See https://golang.org/issue/3795 if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
return resp, urlErr io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
} }
if resp != nil {
resp.Body.Close() resp.Body.Close()
} }
return nil, urlErr
} }
func defaultCheckRedirect(req *Request, via []*Request) error { func defaultCheckRedirect(req *Request, via []*Request) error {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment