-
-
Save dmichael/5710968 to your computer and use it in GitHub Desktop.
package httpclient | |
import ( | |
"net" | |
"net/http" | |
"time" | |
) | |
type Config struct { | |
ConnectTimeout time.Duration | |
ReadWriteTimeout time.Duration | |
} | |
func TimeoutDialer(config *Config) func(net, addr string) (c net.Conn, err error) { | |
return func(netw, addr string) (net.Conn, error) { | |
conn, err := net.DialTimeout(netw, addr, config.ConnectTimeout) | |
if err != nil { | |
return nil, err | |
} | |
conn.SetDeadline(time.Now().Add(config.ReadWriteTimeout)) | |
return conn, nil | |
} | |
} | |
func NewTimeoutClient(args ...interface{}) *http.Client { | |
// Default configuration | |
config := &Config{ | |
ConnectTimeout: 1 * time.Second, | |
ReadWriteTimeout: 1 * time.Second, | |
} | |
// merge the default with user input if there is one | |
if len(args) == 1 { | |
timeout := args[0].(time.Duration) | |
config.ConnectTimeout = timeout | |
config.ReadWriteTimeout = timeout | |
} | |
if len(args) == 2 { | |
config.ConnectTimeout = args[0].(time.Duration) | |
config.ReadWriteTimeout = args[1].(time.Duration) | |
} | |
return &http.Client{ | |
Transport: &http.Transport{ | |
Dial: TimeoutDialer(config), | |
}, | |
} | |
} |
package httpclient | |
import ( | |
"io" | |
"net" | |
"net/http" | |
"sync" | |
"testing" | |
"time" | |
) | |
var starter sync.Once | |
var addr net.Addr | |
func testHandler(w http.ResponseWriter, req *http.Request) { | |
time.Sleep(500 * time.Millisecond) | |
io.WriteString(w, "hello, world!\n") | |
} | |
func testDelayedHandler(w http.ResponseWriter, req *http.Request) { | |
time.Sleep(2100 * time.Millisecond) | |
io.WriteString(w, "hello, world ... in a bit\n") | |
} | |
func setupMockServer(t *testing.T) { | |
http.HandleFunc("/test", testHandler) | |
http.HandleFunc("/test-delayed", testDelayedHandler) | |
ln, err := net.Listen("tcp", ":0") | |
if err != nil { | |
t.Fatalf("failed to listen - %s", err.Error()) | |
} | |
go func() { | |
err = http.Serve(ln, nil) | |
if err != nil { | |
t.Fatalf("failed to start HTTP server - %s", err.Error()) | |
} | |
}() | |
addr = ln.Addr() | |
} | |
func TestDefaultConfig(t *testing.T) { | |
starter.Do(func() { setupMockServer(t) }) | |
httpClient := NewTimeoutClient() | |
req, _ := http.NewRequest("GET", "http://"+addr.String()+"/test-delayed", nil) | |
httpClient = NewTimeoutClient() | |
_, err := httpClient.Do(req) | |
if err == nil { | |
t.Fatalf("request should have timed out") | |
} | |
} | |
func TestHttpClient(t *testing.T) { | |
starter.Do(func() { setupMockServer(t) }) | |
httpClient := NewTimeoutClient() | |
req, _ := http.NewRequest("GET", "http://"+addr.String()+"/test", nil) | |
resp, err := httpClient.Do(req) | |
if err != nil { | |
t.Fatalf("1st request failed - %s", err.Error()) | |
} | |
defer resp.Body.Close() | |
connectTimeout := (250 * time.Millisecond) | |
readWriteTimeout := (50 * time.Millisecond) | |
httpClient = NewTimeoutClient(connectTimeout, readWriteTimeout) | |
resp, err = httpClient.Do(req) | |
if err == nil { | |
t.Fatalf("2nd request should have timed out") | |
} | |
resp, err = httpClient.Do(req) | |
if resp != nil { | |
t.Fatalf("3nd request should not have timed out") | |
} | |
} |
/* | |
This wrapper takes care of both the connection timeout and the readwrite timeout. | |
WARNING: You must instantiate this every time you want to use it, otherwise it is | |
likely that the timeout is reached before you actually make the call. | |
One argument sets the connect timeout and the readwrite timeout to the same value. | |
Other wise, 2 arguments are 1) connect and 2) readwrite | |
It returns an *http.Client | |
*/ | |
package main | |
import( | |
"httpclient" | |
"time" | |
) | |
func main() { | |
httpClient := httpclient.NewWithTimeout(500*time.Millisecond, 1*time.Second) | |
resp, err := httpClient.Get("http://google.com") | |
if err != nil { | |
fmt.Println("Rats! Google is down.") | |
} | |
} |
Hi, thank you for you code.
But I found a little problem when use this set timeout logic with keep-alive connection.
Because the http package reuse the backend TCP/IP connection, when the http connection is keep-alive.
So, when a connection reused after a few seconds. http.Get()
will returns a timeout error.
This is my solution: a TimeoutConn
from Dial
callback.
http://golang.org/pkg/net/http/httptest/ is dope y'all
Hey, check out my fork: http://gist.github.com/seantalts/11266762
The method here doesn't work with keepalive (you get random timeouts during regular, working requests) and @idada 's method has nondeterministic timeouts and lets idle connections timeout, but mine addresses both of those issues in kind of a basic way I think. Updated tests to use httptest and test that keepalive connections are kept alive as well as that timeouts are working properly.
Thanks guys for the comments and the extensions. @seantalts thanks for the reference to httptest (duh). I'm back in Go-land and will give your client a try for a project I am working on.
Wow! httptest IS dope. Good find thanks for pointing it out and thanks for all your Gists guys!
TimeoutDialer have problem in go.1.16 ;
it will cause i/o timeout , in a short duration , like 50 ms , but we set it more than 1 second.
because the code not using keepAlive and golang will using conn cache pool for sites.
so we should use http.Client{ Timeout: XXX , ... } to set one request timeout .
not this!!!!!!!!
mark