HTTP Client Configuration
HTTP Client Configuration Overview
The Go http.Client is highly configurable. Proper configuration ensures reliability, performance, and security.
Basic HTTP Client
Default Client
package client
import (
"net/http"
"time"
)
func NewClient(apiKey string) (*Client, error) {
return &Client{
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}, nil
}
Problems with default:
- No connection pooling control
- Default TLS settings
- No retry logic
- Simple timeout
Timeouts Configuration
Types of Timeouts
func NewClient(apiKey string) (*Client, error) {
transport := &http.Transport{
// DNS resolution timeout
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
// TLS handshake timeout
TLSHandshakeTimeout: 10 * time.Second,
// Wait for response headers timeout
ResponseHeaderTimeout: 20 * time.Second,
// Wait for full response timeout
ExpectContinueTimeout: 1 * time.Second,
}
return &Client{
apiKey: apiKey,
httpClient: &http.Client{
// Overall request timeout
Timeout: 60 * time.Second,
Transport: transport,
},
}, nil
}
Timeout Hierarchy
Client.Timeout (60s) ← Overall limit
├─ Dial (10s) ← Connection establishment
├─ TLS Handshake (10s) ← TLS negotiation
├─ Response Headers (20s) ← Wait for headers
└─ Body Read (remaining) ← Read response body
Context Timeout
func (c *Client) GetDatabase(ctx context.Context, id string) (*Database, error) {
// Operation-specific timeout
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := c.httpClient.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("request timed out after 10s")
}
return nil, err
}
// ...
}
Connection Pooling
Configure Pools
func NewClient(apiKey string) (*Client, error) {
transport := &http.Transport{
// Maximum idle connections across all hosts
MaxIdleConns: 100,
// Maximum idle connections per host
MaxIdleConnsPerHost: 10,
// Maximum total connections per host
MaxConnsPerHost: 50,
// How long to keep idle connections
IdleConnTimeout: 90 * time.Second,
// Disable compression if transferring pre-compressed data
DisableCompression: false,
// Disable keep-alive
DisableKeepAlives: false,
}
return &Client{
httpClient: &http.Client{
Transport: transport,
},
}, nil
}
Connection Reuse Benefits
// ✅ Good: Reuse connections
func BenchmarkWithConnectionPool(b *testing.B) {
client := &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: 10,
},
}
for i := 0; i < b.N; i++ {
resp, _ := client.Get("https://api.example.com/test")
resp.Body.Close()
}
}
// ❌ Bad: New connection each time
func BenchmarkWithoutConnectionPool(b *testing.B) {
for i := 0; i < b.N; i++ {
resp, _ := http.Get("https://api.example.com/test")
resp.Body.Close()
}
}
// Results:
// WithConnectionPool: 1000 ns/op
// WithoutConnectionPool: 50000 ns/op (50x slower!)
TLS Configuration
Custom TLS Config
import (
"crypto/tls"
"crypto/x509"
)
func NewClient(apiKey string, opts *ClientOptions) (*Client, error) {
tlsConfig := &tls.Config{
// Minimum TLS version
MinVersion: tls.VersionTLS12,
// Preferred cipher suites
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
// Certificate verification
InsecureSkipVerify: false,
}
// Add custom CA certificates
if opts.CACertPath != "" {
caCert, err := os.ReadFile(opts.CACertPath)
if err != nil {
return nil, fmt.Errorf("failed to read CA cert: %w", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
return &Client{
httpClient: &http.Client{
Transport: transport,
},
}, nil
}
Client Certificate Authentication
func NewClientWithCert(certFile, keyFile string) (*Client, error) {
// Load client certificate
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("failed to load client cert: %w", err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
return &Client{
httpClient: &http.Client{
Transport: transport,
},
}, nil
}
Skip TLS Verification (Development Only)
// ⚠️ WARNING: Only for development/testing
func NewInsecureClient(apiKey string) (*Client, error) {
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // Skip certificate verification
},
}
return &Client{
apiKey: apiKey,
httpClient: &http.Client{
Transport: transport,
},
}, nil
}
Custom Transport
Logging Transport
type LoggingTransport struct {
Transport http.RoundTripper
Logger *log.Logger
}
func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Log request
t.Logger.Printf("[%s] %s", req.Method, req.URL.String())
start := time.Now()
// Make request
resp, err := t.Transport.RoundTrip(req)
duration := time.Since(start)
if err != nil {
t.Logger.Printf("Request failed: %v (took %v)", err, duration)
return nil, err
}
// Log response
t.Logger.Printf("[%d] %s (took %v)", resp.StatusCode, req.URL.String(), duration)
return resp, nil
}
func NewClient(apiKey string) (*Client, error) {
baseTransport := &http.Transport{
MaxIdleConns: 100,
}
loggingTransport := &LoggingTransport{
Transport: baseTransport,
Logger: log.New(os.Stdout, "HTTP: ", log.LstdFlags),
}
return &Client{
httpClient: &http.Client{
Transport: loggingTransport,
},
}, nil
}
Retry Transport
type RetryTransport struct {
Transport http.RoundTripper
MaxRetries int
}
func (t *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for attempt := 0; attempt <= t.MaxRetries; attempt++ {
// Clone request body for retries
var bodyBytes []byte
if req.Body != nil {
bodyBytes, _ = io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
resp, err = t.Transport.RoundTrip(req)
// Success
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
// Retry on 5xx or network errors
if attempt < t.MaxRetries {
waitTime := time.Duration(math.Pow(2, float64(attempt))) * time.Second
time.Sleep(waitTime)
// Reset body for retry
if bodyBytes != nil {
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
}
}
return resp, err
}
Chaining Transports
func NewClient(apiKey string) (*Client, error) {
// Base transport
baseTransport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
}
// Add retry logic
retryTransport := &RetryTransport{
Transport: baseTransport,
MaxRetries: 3,
}
// Add logging
loggingTransport := &LoggingTransport{
Transport: retryTransport,
Logger: log.New(os.Stdout, "HTTP: ", log.LstdFlags),
}
// Add authentication
authTransport := &AuthTransport{
Transport: loggingTransport,
APIKey: apiKey,
}
return &Client{
httpClient: &http.Client{
Transport: authTransport,
Timeout: 60 * time.Second,
},
}, nil
}
// Request flow:
// Client → AuthTransport → LoggingTransport → RetryTransport → BaseTransport → API
Proxy Configuration
HTTP Proxy
func NewClientWithProxy(apiKey, proxyURL string) (*Client, error) {
proxy, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("invalid proxy URL: %w", err)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxy),
}
return &Client{
httpClient: &http.Client{
Transport: transport,
},
}, nil
}
Proxy from Environment
func NewClient(apiKey string) (*Client, error) {
transport := &http.Transport{
// Automatically use HTTP_PROXY, HTTPS_PROXY, NO_PROXY env vars
Proxy: http.ProxyFromEnvironment,
}
return &Client{
httpClient: &http.Client{
Transport: transport,
},
}, nil
}
Production Configuration
Complete Production Client
package client
import (
"crypto/tls"
"net"
"net/http"
"time"
)
type ClientConfig struct {
APIKey string
BaseURL string
Timeout time.Duration
MaxRetries int
MaxIdleConns int
TLSMinVersion uint16
CACertPath string
SkipTLSVerify bool
ProxyURL string
}
func NewClient(config *ClientConfig) (*Client, error) {
// Validate config
if config.APIKey == "" {
return nil, fmt.Errorf("API key is required")
}
// Set defaults
if config.Timeout == 0 {
config.Timeout = 60 * time.Second
}
if config.MaxIdleConns == 0 {
config.MaxIdleConns = 100
}
if config.TLSMinVersion == 0 {
config.TLSMinVersion = tls.VersionTLS12
}
// Configure TLS
tlsConfig := &tls.Config{
MinVersion: config.TLSMinVersion,
InsecureSkipVerify: config.SkipTLSVerify,
}
// Add custom CA if provided
if config.CACertPath != "" {
caCert, err := os.ReadFile(config.CACertPath)
if err != nil {
return nil, fmt.Errorf("failed to read CA cert: %w", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
}
// Configure transport
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 20 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: config.MaxIdleConns,
MaxIdleConnsPerHost: 10,
MaxConnsPerHost: 50,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: tlsConfig,
}
// Override proxy if specified
if config.ProxyURL != "" {
proxyURL, err := url.Parse(config.ProxyURL)
if err != nil {
return nil, fmt.Errorf("invalid proxy URL: %w", err)
}
transport.Proxy = http.ProxyURL(proxyURL)
}
// Create HTTP client
httpClient := &http.Client{
Transport: transport,
Timeout: config.Timeout,
}
return &Client{
httpClient: httpClient,
apiKey: config.APIKey,
baseURL: config.BaseURL,
}, nil
}
Usage Example
// Provider configuration
func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config ProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
clientConfig := &client.ClientConfig{
APIKey: config.APIKey.ValueString(),
BaseURL: config.Endpoint.ValueString(),
Timeout: time.Duration(config.Timeout.ValueInt64()) * time.Second,
MaxRetries: int(config.MaxRetries.ValueInt64()),
SkipTLSVerify: config.SkipTLSVerify.ValueBool(),
}
client, err := client.NewClient(clientConfig)
if err != nil {
resp.Diagnostics.AddError("Failed to create client", err.Error())
return
}
resp.DataSourceData = client
resp.ResourceData = client
}
Testing HTTP Configuration
Test with Custom Transport
func TestClient_GetDatabase(t *testing.T) {
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Database{
ID: "db-123",
Name: "test-db",
})
}))
defer server.Close()
// Create client with test server
client, _ := NewClient(&ClientConfig{
APIKey: "test-key",
BaseURL: server.URL,
Timeout: 5 * time.Second,
})
db, err := client.GetDatabase(context.Background(), "db-123")
assert.NoError(t, err)
assert.Equal(t, "db-123", db.ID)
}
Best Practices
1. Configure Timeouts
// ✅ Good: Reasonable timeouts
httpClient := &http.Client{
Timeout: 60 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
}).DialContext,
},
}
// ❌ Bad: No timeout
httpClient := &http.Client{}
// Can hang forever
2. Reuse Connections
// ✅ Good: Connection pooling
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
}
// ❌ Bad: No pooling
transport := &http.Transport{
DisableKeepAlives: true, // New connection every time
}
3. Secure TLS
// ✅ Good: Modern TLS
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
// ❌ Bad: Insecure
tlsConfig := &tls.Config{
InsecureSkipVerify: true, // Never in production!
}
Key Takeaways
- ✅ Always configure timeouts
- ✅ Use connection pooling
- ✅ Enforce minimum TLS version
- ✅ Support custom CA certificates
- ✅ Respect proxy environment variables
- ✅ Chain transports for middleware
- ✅ Test with mock servers
- ✅ Log HTTP operations
- ✅ Never skip TLS verification in production
- ✅ Configure operation-specific timeouts with context
What’s Next?
Learn how to implement authentication patterns including API keys, OAuth, and JWT.
Next: Authentication Patterns →
Previous: ← API Client Architecture
Last updated: July 2, 2026