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.")
}
}
@viney
Copy link

viney commented Dec 9, 2013

mark

@bg5sbk
Copy link

bg5sbk commented Feb 26, 2014

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.

https://gist.github.com/idada/9144886

@seantalts
Copy link

@seantalts
Copy link

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.

@dmichael
Copy link
Author

dmichael commented May 8, 2014

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.

@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