/* Provides an HTTP Transport that implements the `RoundTripper` interface and can be used as a built in replacement for the standard library's, providing: * connection timeouts * request timeouts This is a thin wrapper around `http.Transport` that sets dial timeouts and uses Go's internal timer scheduler to call the Go 1.1+ `CancelRequest()` API. */ package httpclient import ( "crypto/tls" "errors" "io" "net" "net/http" "net/url" "sync" "time" ) // returns the current version of the package func Version() string { return "0.4.1" } // Transport implements the RoundTripper interface and can be used as a replacement // for Go's built in http.Transport implementing end-to-end request timeouts. // // transport := &httpclient.Transport{ // ConnectTimeout: 1*time.Second, // ResponseHeaderTimeout: 5*time.Second, // RequestTimeout: 10*time.Second, // } // defer transport.Close() // // client := &http.Client{Transport: transport} // req, _ := http.NewRequest("GET", "http://127.0.0.1/test", nil) // resp, err := client.Do(req) // if err != nil { // return err // } // defer resp.Body.Close() // type Transport struct { // Proxy specifies a function to return a proxy for a given // *http.Request. If the function returns a non-nil error, the // request is aborted with the provided error. // If Proxy is nil or returns a nil *url.URL, no proxy is used. Proxy func(*http.Request) (*url.URL, error) // Dial specifies the dial function for creating TCP // connections. This will override the Transport's ConnectTimeout and // ReadWriteTimeout settings. // If Dial is nil, a dialer is generated on demand matching the Transport's // options. Dial func(network, addr string) (net.Conn, error) // TLSClientConfig specifies the TLS configuration to use with // tls.Client. If nil, the default configuration is used. TLSClientConfig *tls.Config // DisableKeepAlives, if true, prevents re-use of TCP connections // between different HTTP requests. DisableKeepAlives bool // DisableCompression, if true, prevents the Transport from // requesting compression with an "Accept-Encoding: gzip" // request header when the Request contains no existing // Accept-Encoding value. If the Transport requests gzip on // its own and gets a gzipped response, it's transparently // decoded in the Response.Body. However, if the user // explicitly requested gzip it is not automatically // uncompressed. DisableCompression bool // MaxIdleConnsPerHost, if non-zero, controls the maximum idle // (keep-alive) to keep per-host. If zero, // http.DefaultMaxIdleConnsPerHost is used. MaxIdleConnsPerHost int // ConnectTimeout, if non-zero, is the maximum amount of time a dial will wait for // a connect to complete. ConnectTimeout time.Duration // ResponseHeaderTimeout, if non-zero, specifies the amount of // time to wait for a server's response headers after fully // writing the request (including its body, if any). This // time does not include the time to read the response body. ResponseHeaderTimeout time.Duration // RequestTimeout, if non-zero, specifies the amount of time for the entire // request to complete (including all of the above timeouts + entire response body). // This should never be less than the sum total of the above two timeouts. RequestTimeout time.Duration // ReadWriteTimeout, if non-zero, will set a deadline for every Read and // Write operation on the request connection. ReadWriteTimeout time.Duration // TCPWriteBufferSize, the size of the operating system's write // buffer associated with the connection. TCPWriteBufferSize int // TCPReadBuffserSize, the size of the operating system's read // buffer associated with the connection. TCPReadBufferSize int starter sync.Once transport *http.Transport } // Close cleans up the Transport, currently a no-op func (t *Transport) Close() error { return nil } func (t *Transport) lazyStart() { if t.Dial == nil { t.Dial = func(netw, addr string) (net.Conn, error) { c, err := net.DialTimeout(netw, addr, t.ConnectTimeout) if err != nil { return nil, err } if t.TCPReadBufferSize != 0 || t.TCPWriteBufferSize != 0 { if tcpCon, ok := c.(*net.TCPConn); ok { if t.TCPWriteBufferSize != 0 { if err = tcpCon.SetWriteBuffer(t.TCPWriteBufferSize); err != nil { return nil, err } } if t.TCPReadBufferSize != 0 { if err = tcpCon.SetReadBuffer(t.TCPReadBufferSize); err != nil { return nil, err } } } else { err = errors.New("Not Tcp Connection") return nil, err } } if t.ReadWriteTimeout > 0 { timeoutConn := &rwTimeoutConn{ TCPConn: c.(*net.TCPConn), rwTimeout: t.ReadWriteTimeout, } return timeoutConn, nil } return c, nil } } t.transport = &http.Transport{ Dial: t.Dial, Proxy: t.Proxy, TLSClientConfig: t.TLSClientConfig, DisableKeepAlives: t.DisableKeepAlives, DisableCompression: t.DisableCompression, MaxIdleConnsPerHost: t.MaxIdleConnsPerHost, ResponseHeaderTimeout: t.ResponseHeaderTimeout, } } func (t *Transport) CancelRequest(req *http.Request) { t.starter.Do(t.lazyStart) t.transport.CancelRequest(req) } func (t *Transport) CloseIdleConnections() { t.starter.Do(t.lazyStart) t.transport.CloseIdleConnections() } func (t *Transport) RegisterProtocol(scheme string, rt http.RoundTripper) { t.starter.Do(t.lazyStart) t.transport.RegisterProtocol(scheme, rt) } func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { t.starter.Do(t.lazyStart) if t.RequestTimeout > 0 { timer := time.AfterFunc(t.RequestTimeout, func() { t.transport.CancelRequest(req) }) resp, err = t.transport.RoundTrip(req) if err != nil { timer.Stop() } else { resp.Body = &bodyCloseInterceptor{ReadCloser: resp.Body, timer: timer} } } else { resp, err = t.transport.RoundTrip(req) } return } type bodyCloseInterceptor struct { io.ReadCloser timer *time.Timer } func (bci *bodyCloseInterceptor) Close() error { bci.timer.Stop() return bci.ReadCloser.Close() } // A net.Conn that sets a deadline for every Read or Write operation type rwTimeoutConn struct { *net.TCPConn rwTimeout time.Duration } func (c *rwTimeoutConn) Read(b []byte) (int, error) { err := c.TCPConn.SetDeadline(time.Now().Add(c.rwTimeout)) if err != nil { return 0, err } return c.TCPConn.Read(b) } func (c *rwTimeoutConn) Write(b []byte) (int, error) { err := c.TCPConn.SetDeadline(time.Now().Add(c.rwTimeout)) if err != nil { return 0, err } return c.TCPConn.Write(b) }