Skip to content

Instantly share code, notes, and snippets.

@dmichael
Last active October 18, 2023 20:07
Show Gist options
  • Save dmichael/5710968 to your computer and use it in GitHub Desktop.
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.
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.")
}
}
@tmaiaroto
Copy link

Wow! httptest IS dope. Good find thanks for pointing it out and thanks for all your Gists guys!

@ayanmw
Copy link

ayanmw commented Oct 20, 2021

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!!!!!!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment