Provider Middleware
Middleware Overview
Middleware provides a clean way to handle cross-cutting concerns like logging, retries, authentication, and metrics without cluttering business logic.
HTTP Middleware Pattern
RoundTripper Interface
// http.RoundTripper is the interface for HTTP middleware
type RoundTripper interface {
RoundTrip(*http.Request) (*http.Response, error)
}
Middleware Chain
func NewClient(apiKey string) *Client {
// Base transport
base := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
}
// Build middleware chain
transport := chainMiddleware(base,
NewLoggingMiddleware(),
NewRetryMiddleware(3),
NewMetricsMiddleware(),
NewAuthMiddleware(apiKey),
)
return &Client{
httpClient: &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
},
}
}
func chainMiddleware(base http.RoundTripper, middleware ...Middleware) http.RoundTripper {
transport := base
// Apply in reverse order
for i := len(middleware) - 1; i >= 0; i-- {
transport = middleware[i].Wrap(transport)
}
return transport
}
Logging Middleware
Implementation
type LoggingMiddleware struct {
next http.RoundTripper
}
func NewLoggingMiddleware() Middleware {
return &LoggingMiddleware{}
}
func (m *LoggingMiddleware) Wrap(next http.RoundTripper) http.RoundTripper {
m.next = next
return m
}
func (m *LoggingMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
start := time.Now()
// Log request
tflog.Debug(ctx, "HTTP request",
map[string]interface{}{
"method": req.Method,
"url": req.URL.String(),
"headers": sanitizeHeaders(req.Header),
})
// Execute request
resp, err := m.next.RoundTrip(req)
duration := time.Since(start)
if err != nil {
tflog.Error(ctx, "HTTP request failed",
map[string]interface{}{
"method": req.Method,
"url": req.URL.String(),
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
})
return nil, err
}
// Log response
tflog.Debug(ctx, "HTTP response",
map[string]interface{}{
"method": req.Method,
"url": req.URL.String(),
"status": resp.StatusCode,
"duration_ms": duration.Milliseconds(),
})
return resp, nil
}
func sanitizeHeaders(headers http.Header) map[string]string {
sanitized := make(map[string]string)
for key, values := range headers {
// Mask sensitive headers
if strings.ToLower(key) == "authorization" ||
strings.ToLower(key) == "x-api-key" {
sanitized[key] = "***REDACTED***"
} else {
sanitized[key] = strings.Join(values, ", ")
}
}
return sanitized
}
Retry Middleware
Exponential Backoff
type RetryMiddleware struct {
next http.RoundTripper
maxRetries int
backoff BackoffStrategy
}
type BackoffStrategy interface {
NextDelay(attempt int) time.Duration
}
type ExponentialBackoff struct {
BaseDelay time.Duration
MaxDelay time.Duration
}
func (b *ExponentialBackoff) NextDelay(attempt int) time.Duration {
delay := b.BaseDelay * time.Duration(1<<uint(attempt))
if delay > b.MaxDelay {
delay = b.MaxDelay
}
// Add jitter
jitter := time.Duration(rand.Int63n(int64(delay / 4)))
return delay + jitter
}
func NewRetryMiddleware(maxRetries int) Middleware {
return &RetryMiddleware{
maxRetries: maxRetries,
backoff: &ExponentialBackoff{
BaseDelay: 100 * time.Millisecond,
MaxDelay: 5 * time.Second,
},
}
}
func (m *RetryMiddleware) Wrap(next http.RoundTripper) http.RoundTripper {
m.next = next
return m
}
func (m *RetryMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
var resp *http.Response
var err error
for attempt := 0; attempt <= m.maxRetries; attempt++ {
// Clone request for retry
clonedReq := req.Clone(ctx)
resp, err = m.next.RoundTrip(clonedReq)
// Success or non-retryable error
if err == nil && !isRetryableStatus(resp.StatusCode) {
return resp, nil
}
// Don't retry on last attempt
if attempt == m.maxRetries {
break
}
// Determine if we should retry
if !shouldRetry(resp, err) {
break
}
// Calculate backoff
delay := m.backoff.NextDelay(attempt)
tflog.Debug(ctx, "Retrying request",
map[string]interface{}{
"attempt": attempt + 1,
"max_retries": m.maxRetries,
"delay_ms": delay.Milliseconds(),
})
// Wait before retry
select {
case <-time.After(delay):
// Continue to next attempt
case <-ctx.Done():
return nil, ctx.Err()
}
}
return resp, err
}
func isRetryableStatus(statusCode int) bool {
switch statusCode {
case 429, // Too Many Requests
500, // Internal Server Error
502, // Bad Gateway
503, // Service Unavailable
504: // Gateway Timeout
return true
}
return false
}
func shouldRetry(resp *http.Response, err error) bool {
// Network error - retry
if err != nil {
if isTemporaryError(err) {
return true
}
return false
}
// HTTP status - retry on server errors and rate limits
return isRetryableStatus(resp.StatusCode)
}
func isTemporaryError(err error) bool {
if netErr, ok := err.(net.Error); ok {
return netErr.Temporary() || netErr.Timeout()
}
return false
}
Metrics Middleware
Request Metrics
type MetricsMiddleware struct {
next http.RoundTripper
// Metrics counters
requestCount *sync.Map // method+status -> count
requestDuration *sync.Map // method -> []duration
mu sync.RWMutex
}
func NewMetricsMiddleware() Middleware {
return &MetricsMiddleware{
requestCount: &sync.Map{},
requestDuration: &sync.Map{},
}
}
func (m *MetricsMiddleware) Wrap(next http.RoundTripper) http.RoundTripper {
m.next = next
return m
}
func (m *MetricsMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
resp, err := m.next.RoundTrip(req)
duration := time.Since(start)
// Record metrics
m.recordRequest(req.Method, getStatusCode(resp, err), duration)
return resp, err
}
func (m *MetricsMiddleware) recordRequest(method string, statusCode int, duration time.Duration) {
// Increment request count
key := fmt.Sprintf("%s:%d", method, statusCode)
if val, ok := m.requestCount.Load(key); ok {
count := val.(int64)
m.requestCount.Store(key, count+1)
} else {
m.requestCount.Store(key, int64(1))
}
// Record duration
if val, ok := m.requestDuration.Load(method); ok {
durations := val.([]time.Duration)
m.requestDuration.Store(method, append(durations, duration))
} else {
m.requestDuration.Store(method, []time.Duration{duration})
}
}
func (m *MetricsMiddleware) GetMetrics() map[string]interface{} {
metrics := make(map[string]interface{})
// Request counts
counts := make(map[string]int64)
m.requestCount.Range(func(key, value interface{}) bool {
counts[key.(string)] = value.(int64)
return true
})
metrics["request_counts"] = counts
// Duration stats
durations := make(map[string]map[string]interface{})
m.requestDuration.Range(func(key, value interface{}) bool {
method := key.(string)
durs := value.([]time.Duration)
stats := calculateStats(durs)
durations[method] = map[string]interface{}{
"count": len(durs),
"min_ms": stats.Min.Milliseconds(),
"max_ms": stats.Max.Milliseconds(),
"avg_ms": stats.Avg.Milliseconds(),
"p50_ms": stats.P50.Milliseconds(),
"p95_ms": stats.P95.Milliseconds(),
"p99_ms": stats.P99.Milliseconds(),
}
return true
})
metrics["durations"] = durations
return metrics
}
type DurationStats struct {
Min time.Duration
Max time.Duration
Avg time.Duration
P50 time.Duration
P95 time.Duration
P99 time.Duration
}
func calculateStats(durations []time.Duration) DurationStats {
if len(durations) == 0 {
return DurationStats{}
}
// Sort for percentiles
sorted := make([]time.Duration, len(durations))
copy(sorted, durations)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i] < sorted[j]
})
var total time.Duration
for _, d := range sorted {
total += d
}
return DurationStats{
Min: sorted[0],
Max: sorted[len(sorted)-1],
Avg: total / time.Duration(len(sorted)),
P50: sorted[len(sorted)*50/100],
P95: sorted[len(sorted)*95/100],
P99: sorted[len(sorted)*99/100],
}
}
func getStatusCode(resp *http.Response, err error) int {
if err != nil {
return 0
}
return resp.StatusCode
}
Authentication Middleware
API Key Authentication
type AuthMiddleware struct {
next http.RoundTripper
apiKey string
}
func NewAuthMiddleware(apiKey string) Middleware {
return &AuthMiddleware{
apiKey: apiKey,
}
}
func (m *AuthMiddleware) Wrap(next http.RoundTripper) http.RoundTripper {
m.next = next
return m
}
func (m *AuthMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
// Clone request to avoid modifying original
clonedReq := req.Clone(req.Context())
// Add authentication header
clonedReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", m.apiKey))
clonedReq.Header.Set("User-Agent", "terraform-provider-cachecloud/1.0")
return m.next.RoundTrip(clonedReq)
}
OAuth2 Token Refresh
type OAuth2Middleware struct {
next http.RoundTripper
tokenSource oauth2.TokenSource
mu sync.RWMutex
currentToken *oauth2.Token
}
func NewOAuth2Middleware(tokenSource oauth2.TokenSource) Middleware {
return &OAuth2Middleware{
tokenSource: tokenSource,
}
}
func (m *OAuth2Middleware) Wrap(next http.RoundTripper) http.RoundTripper {
m.next = next
return m
}
func (m *OAuth2Middleware) RoundTrip(req *http.Request) (*http.Response, error) {
// Get current token
token, err := m.getToken()
if err != nil {
return nil, fmt.Errorf("failed to get auth token: %w", err)
}
// Clone and add auth header
clonedReq := req.Clone(req.Context())
clonedReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
resp, err := m.next.RoundTrip(clonedReq)
// If 401, try refreshing token once
if err == nil && resp.StatusCode == http.StatusUnauthorized {
// Force token refresh
m.mu.Lock()
m.currentToken = nil
m.mu.Unlock()
token, err = m.getToken()
if err != nil {
return resp, nil // Return original 401
}
// Retry with new token
retryReq := req.Clone(req.Context())
retryReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
return m.next.RoundTrip(retryReq)
}
return resp, err
}
func (m *OAuth2Middleware) getToken() (*oauth2.Token, error) {
m.mu.RLock()
token := m.currentToken
m.mu.RUnlock()
// Check if we have a valid token
if token != nil && token.Valid() {
return token, nil
}
// Get new token
m.mu.Lock()
defer m.mu.Unlock()
// Double-check after acquiring write lock
if m.currentToken != nil && m.currentToken.Valid() {
return m.currentToken, nil
}
token, err := m.tokenSource.Token()
if err != nil {
return nil, err
}
m.currentToken = token
return token, nil
}
Rate Limiting Middleware
Token Bucket Implementation
type RateLimitMiddleware struct {
next http.RoundTripper
limiter *rate.Limiter
}
func NewRateLimitMiddleware(requestsPerSecond float64, burst int) Middleware {
return &RateLimitMiddleware{
limiter: rate.NewLimiter(rate.Limit(requestsPerSecond), burst),
}
}
func (m *RateLimitMiddleware) Wrap(next http.RoundTripper) http.RoundTripper {
m.next = next
return m
}
func (m *RateLimitMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
// Wait for rate limiter
if err := m.limiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("rate limit wait failed: %w", err)
}
return m.next.RoundTrip(req)
}
Response Caching Middleware
Cache Implementation
type CachingMiddleware struct {
next http.RoundTripper
cache *sync.Map
ttl time.Duration
}
type CachedResponse struct {
StatusCode int
Headers http.Header
Body []byte
ExpiresAt time.Time
}
func NewCachingMiddleware(ttl time.Duration) Middleware {
return &CachingMiddleware{
cache: &sync.Map{},
ttl: ttl,
}
}
func (m *CachingMiddleware) Wrap(next http.RoundTripper) http.RoundTripper {
m.next = next
return m
}
func (m *CachingMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
// Only cache GET requests
if req.Method != http.MethodGet {
return m.next.RoundTrip(req)
}
cacheKey := req.URL.String()
// Check cache
if cached, found := m.cache.Load(cacheKey); found {
cachedResp := cached.(*CachedResponse)
if time.Now().Before(cachedResp.ExpiresAt) {
tflog.Debug(req.Context(), "Cache hit", map[string]interface{}{
"url": cacheKey,
})
return reconstructResponse(cachedResp), nil
}
m.cache.Delete(cacheKey)
}
// Execute request
resp, err := m.next.RoundTrip(req)
if err != nil {
return nil, err
}
// Cache successful responses
if resp.StatusCode == http.StatusOK {
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
// Store in cache
m.cache.Store(cacheKey, &CachedResponse{
StatusCode: resp.StatusCode,
Headers: resp.Header.Clone(),
Body: body,
ExpiresAt: time.Now().Add(m.ttl),
})
// Reconstruct response
resp.Body = io.NopCloser(bytes.NewReader(body))
}
return resp, nil
}
func reconstructResponse(cached *CachedResponse) *http.Response {
return &http.Response{
StatusCode: cached.StatusCode,
Header: cached.Headers.Clone(),
Body: io.NopCloser(bytes.NewReader(cached.Body)),
}
}
Complete Middleware Stack
Integration Example
package provider
import (
"net/http"
"time"
"golang.org/x/time/rate"
)
func (p *provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config providerModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
// Build HTTP client with middleware stack
client := buildHTTPClient(
config.APIKey.ValueString(),
config.MaxRetries.ValueInt64(),
config.RateLimit.ValueFloat64(),
)
// Create API client
apiClient := NewAPIClient(client, config.BaseURL.ValueString())
// Make available to resources and data sources
resp.ResourceData = apiClient
resp.DataSourceData = apiClient
}
func buildHTTPClient(apiKey string, maxRetries int64, rateLimit float64) *http.Client {
// Base transport with connection pooling
baseTransport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
// Build middleware chain (applied in reverse order)
transport := chainMiddleware(baseTransport,
NewLoggingMiddleware(), // Innermost: logs after all processing
NewMetricsMiddleware(), // Collect metrics
NewRetryMiddleware(int(maxRetries)), // Retry failed requests
NewRateLimitMiddleware(rateLimit, 10), // Rate limit outgoing requests
NewCachingMiddleware(5*time.Minute), // Cache GET responses
NewAuthMiddleware(apiKey), // Outermost: add auth to all requests
)
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
}
Best Practices
1. Middleware Order Matters
// ✅ Good: Logical ordering
Auth → Cache → RateLimit → Retry → Metrics → Logging
// ❌ Bad: Illogical ordering
Cache → Auth // Cache before auth adds unauthenticated requests
Logging → Retry // Logs won't show retry attempts
2. Context Cancellation
// ✅ Good: Respect context
select {
case <-time.After(delay):
// Continue
case <-ctx.Done():
return nil, ctx.Err()
}
// ❌ Bad: Ignore context
time.Sleep(delay) // Blocks even if context cancelled
3. Thread Safety
// ✅ Good: Protect shared state
m.mu.Lock()
m.cache[key] = value
m.mu.Unlock()
// ❌ Bad: Race condition
m.cache[key] = value // Concurrent access without locking
Key Takeaways
- ✅ Use middleware for cross-cutting concerns
- ✅ Chain middleware in logical order
- ✅ Implement logging for observability
- ✅ Add retry logic with exponential backoff
- ✅ Collect metrics for monitoring
- ✅ Handle authentication consistently
- ✅ Respect context cancellation
- ✅ Ensure thread safety
What’s Next?
Learn advanced testing patterns for complex scenarios.
Next: Advanced Testing Patterns →
Previous: ← Custom Validation
Next: Advanced Testing Patterns →
Last updated: July 1, 2026