Always configure a http.Client with a Timeout or a Transport containing DialTimeout, TLSHandshakeTimeout, and ResponseHeaderTimeout to prevent your application from hanging indefinitely on slow or unresponsive servers. Relying solely on the top-level Timeout is usually sufficient for most use cases, but setting specific transport timeouts gives you finer control over connection phases.
Here is the standard approach using the top-level Timeout field, which covers the entire request lifecycle including DNS lookup, TCP connection, TLS handshake, and reading the response body:
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
// Create a client with a 5-second timeout for the entire request
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get("https://httpbin.org/delay/10")
if err != nil {
// This will print a timeout error because the server delays for 10s
fmt.Printf("Request failed: %v\n", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %s, Body length: %d\n", resp.Status, len(body))
}
If you need granular controlβsuch as failing fast on DNS lookups while allowing more time for the actual data transferβconfigure the Transport explicitly. This is useful when you want to distinguish between a slow network connection and a slow server response.
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// Configure specific timeouts for different phases
client := &http.Client{
Timeout: 30 * time.Second, // Fallback for the whole request
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second, // Max time to establish TCP connection
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second, // Max time for TLS handshake
ResponseHeaderTimeout: 10 * time.Second, // Max time waiting for headers
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
},
}
// Usage remains the same
_, err := client.Get("https://example.com")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
Note that you must import "net" for the DialContext example. If you omit the Timeout field entirely, the client uses http.DefaultClient, which has no timeout and can block your goroutines forever if a server never responds. Always reuse the configured http.Client instance across requests rather than creating a new one for every call, as this allows the underlying connection pool to function correctly.