Skip to content

Commit 8f13080

Browse files
committed
net/http: allow Client.CheckRedirect to use most recent response
Fixes #10069 Change-Id: I3819ff597d5a0c8e785403bf9d65a054f50655a6 Reviewed-on: https://go-review.googlesource.com/23207 Reviewed-by: Russ Cox <[email protected]>
1 parent 5d92aef commit 8f13080

File tree

4 files changed

+80
-15
lines changed

4 files changed

+80
-15
lines changed

src/net/http/client.go

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ type Client struct {
4747
// method returns both the previous Response (with its Body
4848
// closed) and CheckRedirect's error (wrapped in a url.Error)
4949
// instead of issuing the Request req.
50+
// As a special case, if CheckRedirect returns ErrUseLastResponse,
51+
// then the most recent response is returned with its body
52+
// unclosed, along with a nil error.
5053
//
5154
// If CheckRedirect is nil, the Client uses its default policy,
5255
// which is to stop after 10 consecutive requests.
@@ -417,6 +420,12 @@ func (c *Client) Get(url string) (resp *Response, err error) {
417420

418421
func alwaysFalse() bool { return false }
419422

423+
// ErrUseLastResponse can be returned by Client.CheckRedirect hooks to
424+
// control how redirects are processed. If returned, the next request
425+
// is not sent and the most recent response is returned with its body
426+
// unclosed.
427+
var ErrUseLastResponse = errors.New("net/http: use last response")
428+
420429
// checkRedirect calls either the user's configured CheckRedirect
421430
// function, or the default.
422431
func (c *Client) checkRedirect(req *Request, via []*Request) error {
@@ -467,11 +476,12 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo
467476
}
468477
ireq := reqs[0]
469478
req = &Request{
470-
Method: ireq.Method,
471-
URL: u,
472-
Header: make(Header),
473-
Cancel: ireq.Cancel,
474-
ctx: ireq.ctx,
479+
Method: ireq.Method,
480+
Response: resp,
481+
URL: u,
482+
Header: make(Header),
483+
Cancel: ireq.Cancel,
484+
ctx: ireq.ctx,
475485
}
476486
if ireq.Method == "POST" || ireq.Method == "PUT" {
477487
req.Method = "GET"
@@ -481,7 +491,27 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo
481491
if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
482492
req.Header.Set("Referer", ref)
483493
}
484-
if err := c.checkRedirect(req, reqs); err != nil {
494+
err = c.checkRedirect(req, reqs)
495+
496+
// Sentinel error to let users select the
497+
// previous response, without closing its
498+
// body. See Issue 10069.
499+
if err == ErrUseLastResponse {
500+
return resp, nil
501+
}
502+
503+
// Close the previous response's body. But
504+
// read at least some of the body so if it's
505+
// small the underlying TCP connection will be
506+
// re-used. No need to check for errors: if it
507+
// fails, the Transport won't reuse it anyway.
508+
const maxBodySlurpSize = 2 << 10
509+
if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
510+
io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
511+
}
512+
resp.Body.Close()
513+
514+
if err != nil {
485515
// Special case for Go 1 compatibility: return both the response
486516
// and an error if the CheckRedirect function failed.
487517
// See https://golang.org/issue/3795
@@ -508,14 +538,6 @@ func (c *Client) doFollowingRedirects(req *Request, shouldRedirect func(int) boo
508538
if !shouldRedirect(resp.StatusCode) {
509539
return resp, nil
510540
}
511-
512-
// Read the body if small so underlying TCP connection will be re-used.
513-
// No need to check for errors: if it fails, Transport won't reuse it anyway.
514-
const maxBodySlurpSize = 2 << 10
515-
if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
516-
io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
517-
}
518-
resp.Body.Close()
519541
}
520542
}
521543

src/net/http/client_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,44 @@ func TestPostRedirects(t *testing.T) {
366366
}
367367
}
368368

369+
func TestClientRedirectUseResponse(t *testing.T) {
370+
defer afterTest(t)
371+
const body = "Hello, world."
372+
var ts *httptest.Server
373+
ts = httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
374+
if strings.Contains(r.URL.Path, "/other") {
375+
io.WriteString(w, "wrong body")
376+
} else {
377+
w.Header().Set("Location", ts.URL+"/other")
378+
w.WriteHeader(StatusFound)
379+
io.WriteString(w, body)
380+
}
381+
}))
382+
defer ts.Close()
383+
384+
c := &Client{CheckRedirect: func(req *Request, via []*Request) error {
385+
if req.Response == nil {
386+
t.Error("expected non-nil Request.Response")
387+
}
388+
return ErrUseLastResponse
389+
}}
390+
res, err := c.Get(ts.URL)
391+
if err != nil {
392+
t.Fatal(err)
393+
}
394+
if res.StatusCode != StatusFound {
395+
t.Errorf("status = %d; want %d", res.StatusCode, StatusFound)
396+
}
397+
defer res.Body.Close()
398+
slurp, err := ioutil.ReadAll(res.Body)
399+
if err != nil {
400+
t.Fatal(err)
401+
}
402+
if string(slurp) != body {
403+
t.Errorf("body = %q; want %q", slurp, body)
404+
}
405+
}
406+
369407
var expectedCookies = []*Cookie{
370408
{Name: "ChocolateChip", Value: "tasty"},
371409
{Name: "First", Value: "Hit"},

src/net/http/request.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ type Request struct {
255255
// set, it is undefined whether Cancel is respected.
256256
Cancel <-chan struct{}
257257

258+
// Response is the redirect response which caused this request
259+
// to be created. This field is only populated during client
260+
// redirects.
261+
Response *Response
262+
258263
// ctx is either the client or server context. It should only
259264
// be modified via copying the whole Request using WithContext.
260265
// It is unexported to prevent people from using Context wrong

src/net/http/response.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ type Response struct {
9696
// any trailer values sent by the server.
9797
Trailer Header
9898

99-
// The Request that was sent to obtain this Response.
99+
// Request is the request that was sent to obtain this Response.
100100
// Request's Body is nil (having already been consumed).
101101
// This is only populated for Client requests.
102102
Request *Request

0 commit comments

Comments
 (0)