diff --git a/https/README.md b/https/README.md index 90257f5..c689af4 100644 --- a/https/README.md +++ b/https/README.md @@ -1,9 +1,9 @@ # https (as in secure) -This wrappers and helpers to supplement Go's built-in `net/http` with reasonable and safe defaults. +This wrappers and helpers to supplement Go's built-in `net/http` with reasonable and safe defaults, even when used with `http`. See -- [The complete guide to Go net/http timeouts](https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/) -- [Standard net/http config will break your production environment](https://medium.com/@simonfrey/go-as-in-golang-standard-net-http-config-will-break-your-production-environment-1360871cb72b) -- [Don’t use Go’s default HTTP client (in production)](https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779) +- [The complete guide to Go net/http timeouts](https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/) +- [Standard net/http config will break your production environment](https://medium.com/@simonfrey/go-as-in-golang-standard-net-http-config-will-break-your-production-environment-1360871cb72b) +- [Don’t use Go’s default HTTP client (in production)](https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779) diff --git a/https/default.go b/https/default.go new file mode 100644 index 0000000..876d576 --- /dev/null +++ b/https/default.go @@ -0,0 +1,48 @@ +package https + +import ( + "net" + "net/http" + "time" +) + +// NewDefaultDialer expects connections to be established quickly +func NewDefaultDialer() *net.Dialer { + return &net.Dialer{ + Timeout: 4 * time.Second, + KeepAlive: 25 * time.Second, + } +} + +// NewDefaultTransport expects quick tls termination, but allows for +// sluggish header response times (the server often doesn't know +// what headers to send until the response is nearly ready due to API +// calls, db queries, etc) +func NewDefaultTransport() *http.Transport { + dialer := NewDefaultDialer() + + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dialer.DialContext, + ForceAttemptHTTP2: true, + + MaxConnsPerHost: 100, + MaxIdleConnsPerHost: 10, + MaxIdleConns: 100, + IdleConnTimeout: 59 * time.Second, // less than 59s firewall timeouts + + TLSHandshakeTimeout: 4 * time.Second, + ResponseHeaderTimeout: 20 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} + +// NewDefaultClient creates a new http client with reasonable and safe defaults +// for API services that are expected to connect and reply relatively quickly. +// DO NOT use for long streaming downloads or extremely slow APIs. +func NewDefaultClient() *http.Client { + return &http.Client{ + Timeout: 25 * time.Second, + Transport: NewDefaultTransport(), + } +} diff --git a/https/go.mod b/https/go.mod new file mode 100644 index 0000000..2452069 --- /dev/null +++ b/https/go.mod @@ -0,0 +1,3 @@ +module github.com/therootcompany/golib/https + +go 1.26.5 diff --git a/https/https.go b/https/https.go deleted file mode 100644 index 8e671b1..0000000 --- a/https/https.go +++ /dev/null @@ -1,22 +0,0 @@ -package https - -import ( - "net" - "net/http" - "time" -) - -// NewHTTPClient creates a new http client with reasonable and safe defaults -func NewHTTPClient() *http.Client { - transport := &http.Transport{ - Dial: (&net.Dialer{ - Timeout: 10 * time.Second, - }).Dial, - TLSHandshakeTimeout: 5 * time.Second, - } - client := &http.Client{ - Timeout: time.Second * 5, - Transport: transport, - } - return client -} diff --git a/https/internal.go b/https/internal.go new file mode 100644 index 0000000..0ead536 --- /dev/null +++ b/https/internal.go @@ -0,0 +1,44 @@ +package https + +import ( + "net" + "net/http" + "time" +) + +// NewInternalDialer is aggressive because we expect very low network latency. +func NewInternalDialer() *net.Dialer { + return &net.Dialer{ + Timeout: 2 * time.Second, // plenty for DNS + <200 ms RTT + Happy Eyeballs + KeepAlive: 27 * time.Second, + } +} + +// NewInternalTransport prioritizes fast failure and high connection reuse +// for internal service-to-service traffic. +func NewInternalTransport() *http.Transport { + dialer := NewInternalDialer() + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dialer.DialContext, + ForceAttemptHTTP2: true, + + MaxIdleConns: 200, + MaxIdleConnsPerHost: 50, // higher – few hosts, high concurrency + MaxConnsPerHost: 200, + IdleConnTimeout: 29 * time.Second, + + TLSHandshakeTimeout: 1 * time.Second, + ResponseHeaderTimeout: 11 * time.Second, // still allows modest backend work + ExpectContinueTimeout: 1 * time.Second, + } +} + +// NewInternalClient is for internal APIs that should be fast. +// Do not use for slow batch jobs or external calls. +func NewInternalClient() *http.Client { + return &http.Client{ + Timeout: 13 * time.Second, + Transport: NewInternalTransport(), + } +} diff --git a/https/slow.go b/https/slow.go new file mode 100644 index 0000000..303d35d --- /dev/null +++ b/https/slow.go @@ -0,0 +1,45 @@ +package https + +import ( + "cmp" + "net" + "net/http" + "time" +) + +// NewSlowDialer allows more time for DNS, Happy Eyeballs, and high-latency links. +func NewSlowDialer() *net.Dialer { + return &net.Dialer{ + Timeout: 15 * time.Second, + KeepAlive: 30 * time.Second, + } +} + +// NewSlowTransport is tolerant of slow networks and sluggish servers. +// Still protects against completely hung connections. +func NewSlowTransport() *http.Transport { + dialer := NewSlowDialer() + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dialer.DialContext, + ForceAttemptHTTP2: true, + + MaxIdleConns: 100, + MaxIdleConnsPerHost: 20, + MaxConnsPerHost: 100, + IdleConnTimeout: 89 * time.Second, + + TLSHandshakeTimeout: 15 * time.Second, + ResponseHeaderTimeout: 59 * time.Second, + ExpectContinueTimeout: 2 * time.Second, + } +} + +// NewSlowClient is for requests that may legitimately take a long time. +// Prefer request-level context timeouts when you need finer control. +func NewSlowClient(timeout time.Duration) *http.Client { + return &http.Client{ + Timeout: cmp.Or(timeout, 89*time.Second), + Transport: NewSlowTransport(), + } +}