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.
*/
package main
import(
"httpclient"
"time
)
func main() {
httpClient := httpclient.NewWithTimeout(500*time.Millisecond)
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