Commit 8f130802 authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: allow Client.CheckRedirect to use most recent response

Fixes #10069

Change-Id: I3819ff597d5a0c8e785403bf9d65a054f50655a6
Reviewed-on: https://go-review.googlesource.com/23207Reviewed-by: 's avatarRuss Cox <rsc@golang.org>
parent 5d92aefc
...@@ -47,6 +47,9 @@ type Client struct { ...@@ -47,6 +47,9 @@ type Client struct {
// method returns both the previous Response (with its Body // method returns both the previous Response (with its Body
// closed) and CheckRedirect's error (wrapped in a url.Error) // closed) and CheckRedirect's error (wrapped in a url.Error)
// instead of issuing the Request req. // instead of issuing the Request req.
// As a special case, if CheckRedirect returns ErrUseLastResponse,
// then the most recent response is returned with its body
// unclosed, along with a nil error.
// //
// 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.
...@@ -417,6 +420,12 @@ func (c *Client) Get(url string) (resp *Response, err error) { ...@@ -417,6 +420,12 @@ func (c *Client) Get(url string) (resp *Response, err error) {
func alwaysFalse() bool { return false } func alwaysFalse() bool { return false }
// ErrUseLastResponse can be returned by Client.CheckRedirect hooks to
// control how redirects are processed. If returned, the next request
// is not sent and the most recent response is returned with its body
// unclosed.
var ErrUseLastResponse = errors.New("net/http: use last response")
// checkRedirect calls either the user's configured CheckRedirect // checkRedirect calls either the user's configured CheckRedirect
// function, or the default. // function, or the default.
func (c *Client) checkRedirect(req *Request, via []*Request) error { func (c *Client) checkRedirect(req *Request, via []*Request) error {
...@@ -467,11 +476,12 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo ...@@ -467,11 +476,12 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo
} }
ireq := reqs[0] ireq := reqs[0]
req = &Request{ req = &Request{
Method: ireq.Method, Method: ireq.Method,
URL: u, Response: resp,
Header: make(Header), URL: u,
Cancel: ireq.Cancel, Header: make(Header),
ctx: ireq.ctx, Cancel: ireq.Cancel,
ctx: ireq.ctx,
} }
if ireq.Method == "POST" || ireq.Method == "PUT" { if ireq.Method == "POST" || ireq.Method == "PUT" {
req.Method = "GET" req.Method = "GET"
...@@ -481,7 +491,27 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo ...@@ -481,7 +491,27 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo
if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" { if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
req.Header.Set("Referer", ref) req.Header.Set("Referer", ref)
} }
if err := c.checkRedirect(req, reqs); err != nil { 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 // Special case for Go 1 compatibility: return both the response
// and an error if the CheckRedirect function failed. // and an error if the CheckRedirect function failed.
// See https://golang.org/issue/3795 // See https://golang.org/issue/3795
...@@ -508,14 +538,6 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo ...@@ -508,14 +538,6 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo
if !shouldRedirect(resp.StatusCode) { if !shouldRedirect(resp.StatusCode) {
return resp, nil return resp, nil
} }
// Read the body if small so underlying TCP connection will be re-used.
// 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()
} }
} }
......
...@@ -366,6 +366,44 @@ func TestPostRedirects(t *testing.T) { ...@@ -366,6 +366,44 @@ func TestPostRedirects(t *testing.T) {
} }
} }
func TestClientRedirectUseResponse(t *testing.T) {
defer afterTest(t)
const body = "Hello, world."
var ts *httptest.Server
ts = httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
if strings.Contains(r.URL.Path, "/other") {
io.WriteString(w, "wrong body")
} else {
w.Header().Set("Location", ts.URL+"/other")
w.WriteHeader(StatusFound)
io.WriteString(w, body)
}
}))
defer ts.Close()
c := &Client{CheckRedirect: func(req *Request, via []*Request) error {
if req.Response == nil {
t.Error("expected non-nil Request.Response")
}
return ErrUseLastResponse
}}
res, err := c.Get(ts.URL)
if err != nil {
t.Fatal(err)
}
if res.StatusCode != StatusFound {
t.Errorf("status = %d; want %d", res.StatusCode, StatusFound)
}
defer res.Body.Close()
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if string(slurp) != body {
t.Errorf("body = %q; want %q", slurp, body)
}
}
var expectedCookies = []*Cookie{ var expectedCookies = []*Cookie{
{Name: "ChocolateChip", Value: "tasty"}, {Name: "ChocolateChip", Value: "tasty"},
{Name: "First", Value: "Hit"}, {Name: "First", Value: "Hit"},
......
...@@ -255,6 +255,11 @@ type Request struct { ...@@ -255,6 +255,11 @@ type Request struct {
// set, it is undefined whether Cancel is respected. // set, it is undefined whether Cancel is respected.
Cancel <-chan struct{} Cancel <-chan struct{}
// Response is the redirect response which caused this request
// to be created. This field is only populated during client
// redirects.
Response *Response
// ctx is either the client or server context. It should only // ctx is either the client or server context. It should only
// be modified via copying the whole Request using WithContext. // be modified via copying the whole Request using WithContext.
// It is unexported to prevent people from using Context wrong // It is unexported to prevent people from using Context wrong
......
...@@ -96,7 +96,7 @@ type Response struct { ...@@ -96,7 +96,7 @@ type Response struct {
// any trailer values sent by the server. // any trailer values sent by the server.
Trailer Header Trailer Header
// The Request that was sent to obtain this Response. // Request is the request that was sent to obtain this Response.
// Request's Body is nil (having already been consumed). // Request's Body is nil (having already been consumed).
// This is only populated for Client requests. // This is only populated for Client requests.
Request *Request Request *Request
......
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