Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

@viney viney commented Dec 9, 2013

mark

@bg5sbk

This comment has been minimized.

Copy link

@bg5sbk 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

This comment has been minimized.

Copy link

@seantalts seantalts commented Apr 24, 2014

@seantalts

This comment has been minimized.

Copy link

@seantalts seantalts commented Apr 24, 2014

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

This comment has been minimized.

Copy link
Owner Author

@dmichael 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

This comment has been minimized.

Copy link

@tmaiaroto tmaiaroto commented Aug 21, 2014

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.