Skip to content

HTTP Client Middleware Chain

Authors
Matt Cockayne
Date
31 March 2026
Status
DRAFT

Overview

The HTTP server has a composable middleware chain (NewChain, Append, Then). The HTTP client has retry support but no equivalent middleware pattern. Tool authors making API calls commonly need: request logging, auth header injection, rate limiting, and response caching โ€” each as composable, reusable layers.

This spec adds a RoundTripper middleware chain to pkg/http that mirrors the server-side pattern, allowing tool authors to compose client behaviours without subclassing or wrapping http.Client manually.


Design

ClientMiddleware Type

// ClientMiddleware wraps an http.RoundTripper with additional behaviour.
type ClientMiddleware func(next http.RoundTripper) http.RoundTripper

ClientChain Type

// ClientChain composes ClientMiddleware in order. The first middleware
// is the outermost wrapper (executes first on request, last on response).
type ClientChain struct {
    middlewares []ClientMiddleware
}

func NewClientChain(middlewares ...ClientMiddleware) ClientChain
func (c ClientChain) Append(middlewares ...ClientMiddleware) ClientChain
func (c ClientChain) Then(rt http.RoundTripper) http.RoundTripper

Integration with NewClient

// WithClientMiddleware applies a middleware chain to the client's transport.
func WithClientMiddleware(chain ClientChain) ClientOption

Built-in Client Middleware

Request Logging

// WithRequestLogging logs each outbound request and response at the specified level.
func WithRequestLogging(log logger.Logger) ClientMiddleware

Logs: method, URL, status code, duration. Headers and body are NOT logged (security).

Auth Header Injection

// WithBearerToken injects an Authorization: Bearer header on every request.
func WithBearerToken(token string) ClientMiddleware

// WithBasicAuth injects an Authorization: Basic header on every request.
func WithBasicAuth(username, password string) ClientMiddleware

Rate Limiting

// WithRateLimit limits outbound requests to the specified rate.
func WithRateLimit(requestsPerSecond float64) ClientMiddleware

Uses a token bucket algorithm. Blocks until a token is available or the request context is cancelled.


Usage Example

chain := gtbhttp.NewClientChain(
    gtbhttp.WithRequestLogging(props.Logger),
    gtbhttp.WithBearerToken(os.Getenv("API_TOKEN")),
    gtbhttp.WithRateLimit(10),
)

client := gtbhttp.NewClient(
    gtbhttp.WithTimeout(30 * time.Second),
    gtbhttp.WithClientMiddleware(chain),
)

Resolved Questions

  1. Retry refactoring: Leave WithRetry as a separate ClientOption. The retry implementation wraps the transport with backoff logic that requires request body re-reading and idempotency handling โ€” more complex than a simple middleware. Can be migrated later if the middleware pattern proves successful.