-
-
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.") | |
| } | |
| } |
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!!!!!!!!
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
TimeoutConnfromDialcallback.https://gist.github.com/idada/9144886