diff options
Diffstat (limited to 'src/pkg/net/http/client.go')
-rw-r--r-- | src/pkg/net/http/client.go | 111 |
1 files changed, 94 insertions, 17 deletions
diff --git a/src/pkg/net/http/client.go b/src/pkg/net/http/client.go index 22f2e865c..a5a3abe61 100644 --- a/src/pkg/net/http/client.go +++ b/src/pkg/net/http/client.go @@ -14,9 +14,12 @@ import ( "errors" "fmt" "io" + "io/ioutil" "log" "net/url" "strings" + "sync" + "time" ) // A Client is an HTTP client. Its zero value (DefaultClient) is a @@ -52,6 +55,20 @@ type Client struct { // If Jar is nil, cookies are not sent in requests and ignored // in responses. Jar CookieJar + + // Timeout specifies a time limit for requests made by this + // Client. The timeout includes connection time, any + // redirects, and reading the response body. The timer remains + // running after Get, Head, Post, or Do return and will + // interrupt reading of the Response.Body. + // + // A Timeout of zero means no timeout. + // + // The Client's Transport must support the CancelRequest + // method or Client will return errors when attempting to make + // a request with Get, Head, Post, or Do. Client's default + // Transport (DefaultTransport) supports CancelRequest. + Timeout time.Duration } // DefaultClient is the default Client and is used by Get, Head, and Post. @@ -74,8 +91,9 @@ type RoundTripper interface { // authentication, or cookies. // // RoundTrip should not modify the request, except for - // consuming and closing the Body. The request's URL and - // Header fields are guaranteed to be initialized. + // consuming and closing the Body, including on errors. The + // request's URL and Header fields are guaranteed to be + // initialized. RoundTrip(*Request) (*Response, error) } @@ -97,7 +115,7 @@ func (c *Client) send(req *Request) (*Response, error) { req.AddCookie(cookie) } } - resp, err := send(req, c.Transport) + resp, err := send(req, c.transport()) if err != nil { return nil, err } @@ -123,6 +141,9 @@ func (c *Client) send(req *Request) (*Response, error) { // (typically Transport) 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 +// Transport, even on errors. +// // Generally Get, Post, or PostForm will be used instead of Do. func (c *Client) Do(req *Request) (resp *Response, err error) { if req.Method == "GET" || req.Method == "HEAD" { @@ -134,22 +155,28 @@ func (c *Client) Do(req *Request) (resp *Response, err error) { return c.send(req) } +func (c *Client) transport() RoundTripper { + if c.Transport != nil { + return c.Transport + } + return DefaultTransport +} + // send issues an HTTP request. // Caller should close resp.Body when done reading from it. func send(req *Request, t RoundTripper) (resp *Response, err error) { if t == nil { - t = DefaultTransport - if t == nil { - err = errors.New("http: no Client.Transport or DefaultTransport") - return - } + req.closeBody() + return nil, errors.New("http: no Client.Transport or DefaultTransport") } if req.URL == nil { + req.closeBody() return nil, errors.New("http: nil Request.URL") } if req.RequestURI != "" { + req.closeBody() return nil, errors.New("http: Request.RequestURI can't be set in client requests.") } @@ -257,21 +284,40 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo var via []*Request if ireq.URL == nil { + ireq.closeBody() return nil, errors.New("http: nil Request.URL") } + var reqmu sync.Mutex // guards req req := ireq + + var timer *time.Timer + if c.Timeout > 0 { + type canceler interface { + CancelRequest(*Request) + } + tr, ok := c.transport().(canceler) + if !ok { + return nil, fmt.Errorf("net/http: Client Transport of type %T doesn't support CancelRequest; Timeout not supported", c.transport()) + } + timer = time.AfterFunc(c.Timeout, func() { + reqmu.Lock() + defer reqmu.Unlock() + tr.CancelRequest(req) + }) + } + urlStr := "" // next relative or absolute URL to fetch (after first request) redirectFailed := false for redirect := 0; ; redirect++ { if redirect != 0 { - req = new(Request) - req.Method = ireq.Method + nreq := new(Request) + nreq.Method = ireq.Method if ireq.Method == "POST" || ireq.Method == "PUT" { - req.Method = "GET" + nreq.Method = "GET" } - req.Header = make(Header) - req.URL, err = base.Parse(urlStr) + nreq.Header = make(Header) + nreq.URL, err = base.Parse(urlStr) if err != nil { break } @@ -279,15 +325,18 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo // Add the Referer header. lastReq := via[len(via)-1] if lastReq.URL.Scheme != "https" { - req.Header.Set("Referer", lastReq.URL.String()) + nreq.Header.Set("Referer", lastReq.URL.String()) } - err = redirectChecker(req, via) + err = redirectChecker(nreq, via) if err != nil { redirectFailed = true break } } + reqmu.Lock() + req = nreq + reqmu.Unlock() } urlStr = req.URL.String() @@ -296,6 +345,12 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo } if shouldRedirect(resp.StatusCode) { + // 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() if urlStr = resp.Header.Get("Location"); urlStr == "" { err = errors.New(fmt.Sprintf("%d response missing Location header", resp.StatusCode)) @@ -305,7 +360,10 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo via = append(via, req) continue } - return + if timer != nil { + resp.Body = &cancelTimerBody{timer, resp.Body} + } + return resp, nil } method := ireq.Method @@ -349,7 +407,7 @@ func Post(url string, bodyType string, body io.Reader) (resp *Response, err erro // Caller should close resp.Body when done reading from it. // // If the provided body is also an io.Closer, it is closed after the -// body is successfully written to the server. +// request. func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error) { req, err := NewRequest("POST", url, body) if err != nil { @@ -408,3 +466,22 @@ func (c *Client) Head(url string) (resp *Response, err error) { } return c.doFollowingRedirects(req, shouldRedirectGet) } + +type cancelTimerBody struct { + t *time.Timer + rc io.ReadCloser +} + +func (b *cancelTimerBody) Read(p []byte) (n int, err error) { + n, err = b.rc.Read(p) + if err == io.EOF { + b.t.Stop() + } + return +} + +func (b *cancelTimerBody) Close() error { + err := b.rc.Close() + b.t.Stop() + return err +} |