Last active
October 18, 2023 20:07
-
-
Save dmichael/5710968 to your computer and use it in GitHub Desktop.
Light wrapper for the Go http client adding (essential) timeouts for both connect and readwrite.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | |
}, | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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.") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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!!!!!!!!